diff --git a/Dockerfile b/Dockerfile index 5f8c9c625..48b8b90ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM gradle:6.0.1-jdk8 +FROM gradle:6.1.1-jdk8 ENV ANDROID_SDK_URL https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip -ENV ANDROID_API_LEVEL android-29 -ENV ANDROID_BUILD_TOOLS_VERSION 29.0.3 +ENV ANDROID_API_LEVEL android-30 +ENV ANDROID_BUILD_TOOLS_VERSION 30.0.1 ENV ANDROID_HOME /usr/local/android-sdk-linux ENV ANDROID_NDK_VERSION 21.1.6352462 -ENV ANDROID_VERSION 29 +ENV ANDROID_VERSION 30 ENV ANDROID_NDK_HOME ${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}/ ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools diff --git a/TMessagesProj/build.gradle b/TMessagesProj/build.gradle index 265849721..f92b25e00 100644 --- a/TMessagesProj/build.gradle +++ b/TMessagesProj/build.gradle @@ -81,7 +81,8 @@ dependencies { implementation 'androidx.exifinterface:exifinterface:1.2.0' implementation "androidx.interpolator:interpolator:1.0.0" implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0' - implementation 'com.android.support:multidex:1.0.3' + implementation 'androidx.multidex:multidex:2.0.1' + implementation "androidx.sharetarget:sharetarget:1.0.0" // replace zxing with latest // TODO: fix problem with android L @@ -96,7 +97,7 @@ dependencies { implementation 'com.stripe:stripe-android:2.0.2' implementation 'com.google.code.gson:gson:2.8.6' - implementation 'org.osmdroid:osmdroid-android:6.1.7' + implementation 'org.osmdroid:osmdroid-android:6.1.8' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' @@ -105,8 +106,8 @@ dependencies { implementation 'dnsjava:dnsjava:3.2.2' implementation 'org.dizitart:nitrite:3.4.2' - implementation 'cn.hutool:hutool-core:5.3.9' - implementation 'cn.hutool:hutool-crypto:5.3.9' + implementation 'cn.hutool:hutool-core:5.3.10' + implementation 'cn.hutool:hutool-crypto:5.3.10' implementation 'org.tukaani:xz:1.8' implementation project(":openpgp-api") @@ -145,8 +146,8 @@ task writeUpdateInfo { tasks.findByName("preBuild").finalizedBy(writeUpdateInfo) android { - compileSdkVersion 29 - buildToolsVersion '29.0.3' + compileSdkVersion 30 + buildToolsVersion '30.0.1' ndkVersion rootProject.ext.ndkVersion defaultConfig.applicationId = "nekox.messenger" diff --git a/TMessagesProj/jni/Android.mk b/TMessagesProj/jni/Android.mk index bb8fc9929..94b023a57 100644 --- a/TMessagesProj/jni/Android.mk +++ b/TMessagesProj/jni/Android.mk @@ -296,11 +296,11 @@ include $(BUILD_STATIC_LIBRARY) include $(CLEAR_VARS) LOCAL_PRELINK_MODULE := false -LOCAL_MODULE := tmessages.30 +LOCAL_MODULE := tmessages.31 LOCAL_CFLAGS := -w -std=c11 -Os -DNULL=0 -DSOCKLEN_T=socklen_t -DLOCALE_NOT_USED -D_LARGEFILE_SOURCE=1 LOCAL_CFLAGS += -Drestrict='' -D__EMX__ -DOPUS_BUILD -DFIXED_POINT -DUSE_ALLOCA -DHAVE_LRINT -DHAVE_LRINTF -fno-math-errno LOCAL_CFLAGS += -DANDROID_NDK -DDISABLE_IMPORTGL -fno-strict-aliasing -fprefetch-loop-arrays -DAVOID_TABLES -DANDROID_TILE_BASED_DECODE -DANDROID_ARMV6_IDCT -ffast-math -D__STDC_CONSTANT_MACROS -LOCAL_CPPFLAGS := -DBSD=1 -ffast-math -Os -funroll-loops -std=c++14 -DPACKAGE_NAME='"drinkless/org/ton"' +LOCAL_CPPFLAGS := -DBSD=1 -ffast-math -Os -funroll-loops -std=c++14 LOCAL_LDLIBS := -ljnigraphics -llog -lz -lEGL -lGLESv2 -landroid LOCAL_STATIC_LIBRARIES := webp sqlite lz4 rlottie tgnet swscale avformat avcodec avresample avutil swresample flac diff --git a/TMessagesProj/jni/TgNetWrapper.cpp b/TMessagesProj/jni/TgNetWrapper.cpp index b63584689..92a222483 100644 --- a/TMessagesProj/jni/TgNetWrapper.cpp +++ b/TMessagesProj/jni/TgNetWrapper.cpp @@ -404,7 +404,7 @@ void setSystemLangCode(JNIEnv *env, jclass c, jint instanceNum, jstring langCode } } -void init(JNIEnv *env, jclass c, jint instanceNum, jint version, jint layer, jint apiId, jstring deviceModel, jstring systemVersion, jstring appVersion, jstring langCode, jstring systemLangCode, jstring configPath, jstring logPath, jstring regId, jstring cFingerprint, jint timezoneOffset, jint userId, jboolean enablePushConnection, jboolean hasNetwork, jint networkType) { +void init(JNIEnv *env, jclass c, jint instanceNum, jint version, jint layer, jint apiId, jstring deviceModel, jstring systemVersion, jstring appVersion, jstring langCode, jstring systemLangCode, jstring configPath, jstring logPath, jstring regId, jstring cFingerprint, jstring installerId, jint timezoneOffset, jint userId, jboolean enablePushConnection, jboolean hasNetwork, jint networkType) { const char *deviceModelStr = env->GetStringUTFChars(deviceModel, 0); const char *systemVersionStr = env->GetStringUTFChars(systemVersion, 0); const char *appVersionStr = env->GetStringUTFChars(appVersion, 0); @@ -414,8 +414,9 @@ void init(JNIEnv *env, jclass c, jint instanceNum, jint version, jint layer, jin const char *logPathStr = env->GetStringUTFChars(logPath, 0); const char *regIdStr = env->GetStringUTFChars(regId, 0); const char *cFingerprintStr = env->GetStringUTFChars(cFingerprint, 0); + const char *installerIdStr = env->GetStringUTFChars(installerId, 0); - ConnectionsManager::getInstance(instanceNum).init((uint32_t) version, layer, apiId, std::string(deviceModelStr), std::string(systemVersionStr), std::string(appVersionStr), std::string(langCodeStr), std::string(systemLangCodeStr), std::string(configPathStr), std::string(logPathStr), std::string(regIdStr), std::string(cFingerprintStr), timezoneOffset, userId, true, enablePushConnection, hasNetwork, networkType); + ConnectionsManager::getInstance(instanceNum).init((uint32_t) version, layer, apiId, std::string(deviceModelStr), std::string(systemVersionStr), std::string(appVersionStr), std::string(langCodeStr), std::string(systemLangCodeStr), std::string(configPathStr), std::string(logPathStr), std::string(regIdStr), std::string(cFingerprintStr), std::string(installerIdStr), timezoneOffset, userId, true, enablePushConnection, hasNetwork, networkType); if (deviceModelStr != 0) { env->ReleaseStringUTFChars(deviceModel, deviceModelStr); @@ -444,6 +445,9 @@ void init(JNIEnv *env, jclass c, jint instanceNum, jint version, jint layer, jin if (cFingerprintStr != 0) { env->ReleaseStringUTFChars(cFingerprint, cFingerprintStr); } + if (installerIdStr != 0) { + env->ReleaseStringUTFChars(installerId, installerIdStr); + } } void setJava(JNIEnv *env, jclass c, jboolean useJavaByteBuffers) { @@ -472,7 +476,7 @@ static JNINativeMethod ConnectionsManagerMethods[] = { {"native_setProxySettings", "(ILjava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", (void *) setProxySettings}, {"native_getConnectionState", "(I)I", (void *) getConnectionState}, {"native_setUserId", "(II)V", (void *) setUserId}, - {"native_init", "(IIIILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIZZI)V", (void *) init}, + {"native_init", "(IIIILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIZZI)V", (void *) init}, {"native_setLangCode", "(ILjava/lang/String;)V", (void *) setLangCode}, {"native_setRegId", "(ILjava/lang/String;)V", (void *) setRegId}, {"native_setSystemLangCode", "(ILjava/lang/String;)V", (void *) setSystemLangCode}, diff --git a/TMessagesProj/jni/exoplayer/flac_jni.cc b/TMessagesProj/jni/exoplayer/flac_jni.cc index f0a33f323..7c4e25e2f 100644 --- a/TMessagesProj/jni/exoplayer/flac_jni.cc +++ b/TMessagesProj/jni/exoplayer/flac_jni.cc @@ -40,45 +40,45 @@ JNIEnv *env, jobject thiz, ##__VA_ARGS__) class JavaDataSource : public DataSource { - public: - void setFlacDecoderJni(JNIEnv *env, jobject flacDecoderJni) { - this->env = env; - this->flacDecoderJni = flacDecoderJni; - if (mid == NULL) { - jclass cls = env->GetObjectClass(flacDecoderJni); - mid = env->GetMethodID(cls, "read", "(Ljava/nio/ByteBuffer;)I"); +public: + void setFlacDecoderJni(JNIEnv *env, jobject flacDecoderJni) { + this->env = env; + this->flacDecoderJni = flacDecoderJni; + if (mid == NULL) { + jclass cls = env->GetObjectClass(flacDecoderJni); + mid = env->GetMethodID(cls, "read", "(Ljava/nio/ByteBuffer;)I"); + } } - } - ssize_t readAt(off64_t offset, void *const data, size_t size) { - jobject byteBuffer = env->NewDirectByteBuffer(data, size); - int result = env->CallIntMethod(flacDecoderJni, mid, byteBuffer); - if (env->ExceptionCheck()) { - // Exception is thrown in Java when returning from the native call. - result = -1; + ssize_t readAt(off64_t offset, void *const data, size_t size) { + jobject byteBuffer = env->NewDirectByteBuffer(data, size); + int result = env->CallIntMethod(flacDecoderJni, mid, byteBuffer); + if (env->ExceptionCheck()) { + // Exception is thrown in Java when returning from the native call. + result = -1; + } + return result; } - return result; - } - private: - JNIEnv *env; - jobject flacDecoderJni; - jmethodID mid; +private: + JNIEnv *env; + jobject flacDecoderJni; + jmethodID mid; }; struct Context { - JavaDataSource *source; - FLACParser *parser; + JavaDataSource *source; + FLACParser *parser; - Context() { - source = new JavaDataSource(); - parser = new FLACParser(source); - } + Context() { + source = new JavaDataSource(); + parser = new FLACParser(source); + } - ~Context() { - delete parser; - delete source; - } + ~Context() { + delete parser; + delete source; + } }; DECODER_FUNC(jlong, flacInit) { @@ -99,16 +99,16 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { jclass arrayListClass = env->FindClass("java/util/ArrayList"); jmethodID arrayListConstructor = - env->GetMethodID(arrayListClass, "", "()V"); + env->GetMethodID(arrayListClass, "", "()V"); jobject commentList = env->NewObject(arrayListClass, arrayListConstructor); jmethodID arrayListAddMethod = - env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); + env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); if (context->parser->areVorbisCommentsValid()) { std::vector vorbisComments = - context->parser->getVorbisComments(); + context->parser->getVorbisComments(); for (std::vector::const_iterator vorbisComment = - vorbisComments.begin(); + vorbisComments.begin(); vorbisComment != vorbisComments.end(); ++vorbisComment) { jstring commentString = env->NewStringUTF((*vorbisComment).c_str()); env->CallBooleanMethod(commentList, arrayListAddMethod, commentString); @@ -121,10 +121,10 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { if (picturesValid) { std::vector pictures = context->parser->getPictures(); jclass pictureFrameClass = env->FindClass( - "com/google/android/exoplayer2/metadata/flac/PictureFrame"); + "com/google/android/exoplayer2/metadata/flac/PictureFrame"); jmethodID pictureFrameConstructor = - env->GetMethodID(pictureFrameClass, "", - "(ILjava/lang/String;Ljava/lang/String;IIII[B)V"); + env->GetMethodID(pictureFrameClass, "", + "(ILjava/lang/String;Ljava/lang/String;IIII[B)V"); for (std::vector::const_iterator picture = pictures.begin(); picture != pictures.end(); ++picture) { jstring mimeType = env->NewStringUTF(picture->mimeType.c_str()); @@ -133,9 +133,9 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { env->SetByteArrayRegion(pictureData, 0, picture->data.size(), (signed char *)&picture->data[0]); jobject pictureFrame = env->NewObject( - pictureFrameClass, pictureFrameConstructor, picture->type, mimeType, - description, picture->width, picture->height, picture->depth, - picture->colors, pictureData); + pictureFrameClass, pictureFrameConstructor, picture->type, mimeType, + description, picture->width, picture->height, picture->depth, + picture->colors, pictureData); env->CallBooleanMethod(pictureFrames, arrayListAddMethod, pictureFrame); env->DeleteLocalRef(mimeType); env->DeleteLocalRef(description); @@ -144,14 +144,14 @@ DECODER_FUNC(jobject, flacDecodeMetadata, jlong jContext) { } const FLAC__StreamMetadata_StreamInfo &streamInfo = - context->parser->getStreamInfo(); + context->parser->getStreamInfo(); jclass flacStreamMetadataClass = env->FindClass( - "com/google/android/exoplayer2/util/" - "FlacStreamMetadata"); + "com/google/android/exoplayer2/util/" + "FlacStreamMetadata"); jmethodID flacStreamMetadataConstructor = - env->GetMethodID(flacStreamMetadataClass, "", - "(IIIIIIIJLjava/util/List;Ljava/util/List;)V"); + env->GetMethodID(flacStreamMetadataClass, "", + "(IIIIIIIJLjava/util/ArrayList;Ljava/util/ArrayList;)V"); return env->NewObject(flacStreamMetadataClass, flacStreamMetadataConstructor, streamInfo.min_blocksize, streamInfo.max_blocksize, diff --git a/TMessagesProj/jni/exoplayer/flac_parser.cc b/TMessagesProj/jni/exoplayer/flac_parser.cc index 7c69119fe..07fe4e75c 100644 --- a/TMessagesProj/jni/exoplayer/flac_parser.cc +++ b/TMessagesProj/jni/exoplayer/flac_parser.cc @@ -52,31 +52,31 @@ const int endian = 1; // with the same parameter list, but discard redundant information. FLAC__StreamDecoderReadStatus FLACParser::read_callback( - const FLAC__StreamDecoder * /* decoder */, FLAC__byte buffer[], - size_t *bytes, void *client_data) { + const FLAC__StreamDecoder * /* decoder */, FLAC__byte buffer[], + size_t *bytes, void *client_data) { return reinterpret_cast(client_data) - ->readCallback(buffer, bytes); + ->readCallback(buffer, bytes); } FLAC__StreamDecoderSeekStatus FLACParser::seek_callback( - const FLAC__StreamDecoder * /* decoder */, - FLAC__uint64 absolute_byte_offset, void *client_data) { + const FLAC__StreamDecoder * /* decoder */, + FLAC__uint64 absolute_byte_offset, void *client_data) { return reinterpret_cast(client_data) - ->seekCallback(absolute_byte_offset); + ->seekCallback(absolute_byte_offset); } FLAC__StreamDecoderTellStatus FLACParser::tell_callback( - const FLAC__StreamDecoder * /* decoder */, - FLAC__uint64 *absolute_byte_offset, void *client_data) { + const FLAC__StreamDecoder * /* decoder */, + FLAC__uint64 *absolute_byte_offset, void *client_data) { return reinterpret_cast(client_data) - ->tellCallback(absolute_byte_offset); + ->tellCallback(absolute_byte_offset); } FLAC__StreamDecoderLengthStatus FLACParser::length_callback( - const FLAC__StreamDecoder * /* decoder */, FLAC__uint64 *stream_length, - void *client_data) { + const FLAC__StreamDecoder * /* decoder */, FLAC__uint64 *stream_length, + void *client_data) { return reinterpret_cast(client_data) - ->lengthCallback(stream_length); + ->lengthCallback(stream_length); } FLAC__bool FLACParser::eof_callback(const FLAC__StreamDecoder * /* decoder */, @@ -85,10 +85,10 @@ FLAC__bool FLACParser::eof_callback(const FLAC__StreamDecoder * /* decoder */, } FLAC__StreamDecoderWriteStatus FLACParser::write_callback( - const FLAC__StreamDecoder * /* decoder */, const FLAC__Frame *frame, - const FLAC__int32 *const buffer[], void *client_data) { + const FLAC__StreamDecoder * /* decoder */, const FLAC__Frame *frame, + const FLAC__int32 *const buffer[], void *client_data) { return reinterpret_cast(client_data) - ->writeCallback(frame, buffer); + ->writeCallback(frame, buffer); } void FLACParser::metadata_callback(const FLAC__StreamDecoder * /* decoder */, @@ -125,27 +125,27 @@ FLAC__StreamDecoderReadStatus FLACParser::readCallback(FLAC__byte buffer[], } FLAC__StreamDecoderSeekStatus FLACParser::seekCallback( - FLAC__uint64 absolute_byte_offset) { + FLAC__uint64 absolute_byte_offset) { mCurrentPos = absolute_byte_offset; mEOF = false; return FLAC__STREAM_DECODER_SEEK_STATUS_OK; } FLAC__StreamDecoderTellStatus FLACParser::tellCallback( - FLAC__uint64 *absolute_byte_offset) { + FLAC__uint64 *absolute_byte_offset) { *absolute_byte_offset = mCurrentPos; return FLAC__STREAM_DECODER_TELL_STATUS_OK; } FLAC__StreamDecoderLengthStatus FLACParser::lengthCallback( - FLAC__uint64 *stream_length) { + FLAC__uint64 *stream_length) { return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; } FLAC__bool FLACParser::eofCallback() { return mEOF; } FLAC__StreamDecoderWriteStatus FLACParser::writeCallback( - const FLAC__Frame *frame, const FLAC__int32 *const buffer[]) { + const FLAC__Frame *frame, const FLAC__int32 *const buffer[]) { if (mWriteRequested) { mWriteRequested = false; // FLAC parser doesn't free or realloc buffer until next frame or finish @@ -168,21 +168,21 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { } else { ALOGE("FLACParser::metadataCallback unexpected STREAMINFO"); } - break; + break; case FLAC__METADATA_TYPE_SEEKTABLE: mSeekTable = &metadata->data.seek_table; - break; + break; case FLAC__METADATA_TYPE_VORBIS_COMMENT: if (!mVorbisCommentsValid) { FLAC__StreamMetadata_VorbisComment vorbisComment = - metadata->data.vorbis_comment; + metadata->data.vorbis_comment; for (FLAC__uint32 i = 0; i < vorbisComment.num_comments; ++i) { FLAC__StreamMetadata_VorbisComment_Entry vorbisCommentEntry = - vorbisComment.comments[i]; + vorbisComment.comments[i]; if (vorbisCommentEntry.entry != NULL) { std::string comment( - reinterpret_cast(vorbisCommentEntry.entry), - vorbisCommentEntry.length); + reinterpret_cast(vorbisCommentEntry.entry), + vorbisCommentEntry.length); mVorbisComments.push_back(comment); } } @@ -190,14 +190,14 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { } else { ALOGE("FLACParser::metadataCallback unexpected VORBISCOMMENT"); } - break; + break; case FLAC__METADATA_TYPE_PICTURE: { const FLAC__StreamMetadata_Picture *parsedPicture = - &metadata->data.picture; + &metadata->data.picture; FlacPicture picture; picture.mimeType.assign(std::string(parsedPicture->mime_type)); picture.description.assign( - std::string((char *)parsedPicture->description)); + std::string((char *)parsedPicture->description)); picture.data.assign(parsedPicture->data, parsedPicture->data + parsedPicture->data_length); picture.width = parsedPicture->width; @@ -211,7 +211,7 @@ void FLACParser::metadataCallback(const FLAC__StreamMetadata *metadata) { } default: ALOGE("FLACParser::metadataCallback unexpected type %u", metadata->type); - break; + break; } } @@ -232,7 +232,7 @@ static void copyToByteArrayBigEndian(int8_t *dst, const int *const *src, // and then skip the first few bytes (most significant bytes) // depending on the bit depth const int8_t *byteSrc = - reinterpret_cast(&src[c][i]) + 4 - bytesPerSample; + reinterpret_cast(&src[c][i]) + 4 - bytesPerSample; memcpy(dst, byteSrc, bytesPerSample); dst = dst + bytesPerSample; } @@ -262,20 +262,20 @@ static void copyTrespass(int8_t * /* dst */, const int *const * /* src */, // FLACParser FLACParser::FLACParser(DataSource *source) - : mDataSource(source), - mCopy(copyTrespass), - mDecoder(NULL), - mSeekTable(NULL), - firstFrameOffset(0LL), - mCurrentPos(0LL), - mEOF(false), - mStreamInfoValid(false), - mVorbisCommentsValid(false), - mPicturesValid(false), - mWriteRequested(false), - mWriteCompleted(false), - mWriteBuffer(NULL), - mErrorStatus((FLAC__StreamDecoderErrorStatus)-1) { + : mDataSource(source), + mCopy(copyTrespass), + mDecoder(NULL), + mCurrentPos(0LL), + mEOF(false), + mStreamInfoValid(false), + mSeekTable(NULL), + firstFrameOffset(0LL), + mVorbisCommentsValid(false), + mPicturesValid(false), + mWriteRequested(false), + mWriteCompleted(false), + mWriteBuffer(NULL), + mErrorStatus((FLAC__StreamDecoderErrorStatus)-1) { ALOGV("FLACParser::FLACParser"); memset(&mStreamInfo, 0, sizeof(mStreamInfo)); memset(&mWriteHeader, 0, sizeof(mWriteHeader)); @@ -311,9 +311,9 @@ bool FLACParser::init() { FLAC__METADATA_TYPE_PICTURE); FLAC__StreamDecoderInitStatus initStatus; initStatus = FLAC__stream_decoder_init_stream( - mDecoder, read_callback, seek_callback, tell_callback, length_callback, - eof_callback, write_callback, metadata_callback, error_callback, - reinterpret_cast(this)); + mDecoder, read_callback, seek_callback, tell_callback, length_callback, + eof_callback, write_callback, metadata_callback, error_callback, + reinterpret_cast(this)); if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { // A failure here probably indicates a programming error and so is // unlikely to happen. But we check and log here similarly to above. @@ -347,27 +347,7 @@ bool FLACParser::decodeMetadata() { break; default: ALOGE("unsupported bits per sample %u", getBitsPerSample()); - return false; - } - // check sample rate - switch (getSampleRate()) { - case 8000: - case 11025: - case 12000: - case 16000: - case 22050: - case 24000: - case 32000: - case 44100: - case 48000: - case 88200: - case 96000: - case 176400: - case 192000: - break; - default: - ALOGE("unsupported sample rate %u", getSampleRate()); - return false; + return false; } // configure the appropriate copy function based on device endianness. if (isBigEndian()) { @@ -410,11 +390,11 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { mWriteHeader.channels != getChannels() || mWriteHeader.bits_per_sample != getBitsPerSample()) { ALOGE( - "FLACParser::readBuffer write changed parameters mid-stream: %d/%d/%d " - "-> %d/%d/%d", - getSampleRate(), getChannels(), getBitsPerSample(), - mWriteHeader.sample_rate, mWriteHeader.channels, - mWriteHeader.bits_per_sample); + "FLACParser::readBuffer write changed parameters mid-stream: %d/%d/%d " + "-> %d/%d/%d", + getSampleRate(), getChannels(), getBitsPerSample(), + mWriteHeader.sample_rate, mWriteHeader.channels, + mWriteHeader.bits_per_sample); return -1; } @@ -422,9 +402,9 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { size_t bufferSize = blocksize * getChannels() * bytesPerSample; if (bufferSize > output_size) { ALOGE( - "FLACParser::readBuffer not enough space in output buffer " - "%zu < %zu", - output_size, bufferSize); + "FLACParser::readBuffer not enough space in output buffer " + "%zu < %zu", + output_size, bufferSize); return -1; } @@ -456,11 +436,15 @@ bool FLACParser::getSeekPositions(int64_t timeUs, for (unsigned i = length; i != 0; i--) { int64_t sampleNumber = points[i - 1].sample_number; + if (sampleNumber == -1) { // placeholder + continue; + } if (sampleNumber <= targetSampleNumber) { result[0] = (sampleNumber * 1000000LL) / sampleRate; result[1] = firstFrameOffset + points[i - 1].stream_offset; - if (sampleNumber == targetSampleNumber || i >= length) { - // exact seek, or no following seek point. + if (sampleNumber == targetSampleNumber || i >= length || + points[i].sample_number == -1) { // placeholder + // exact seek, or no following non-placeholder seek point result[2] = result[0]; result[3] = result[1]; } else { diff --git a/TMessagesProj/jni/exoplayer/opus_jni.cc b/TMessagesProj/jni/exoplayer/opus_jni.cc index bc50c959e..c7ed61de0 100644 --- a/TMessagesProj/jni/exoplayer/opus_jni.cc +++ b/TMessagesProj/jni/exoplayer/opus_jni.cc @@ -56,14 +56,14 @@ static int channelCount; static int errorCode; DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount, - jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) { + jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) { int status = OPUS_INVALID_STATE; ::channelCount = channelCount; errorCode = 0; jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0); uint8_t* streamMap = reinterpret_cast(streamMapBytes); OpusMSDecoder* decoder = opus_multistream_decoder_create( - sampleRate, channelCount, numStreams, numCoupled, streamMap, &status); + sampleRate, channelCount, numStreams, numCoupled, streamMap, &status); env->ReleaseByteArrayElements(jStreamMap, streamMapBytes, 0); if (!decoder || status != OPUS_OK) { LOGE("Failed to create Opus Decoder; status=%s", opus_strerror(status)); @@ -77,22 +77,22 @@ DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount, // Populate JNI References. const jclass outputBufferClass = env->FindClass( - "com/google/android/exoplayer2/decoder/SimpleOutputBuffer"); + "com/google/android/exoplayer2/decoder/SimpleOutputBuffer"); outputBufferInit = env->GetMethodID(outputBufferClass, "init", - "(JI)Ljava/nio/ByteBuffer;"); + "(JI)Ljava/nio/ByteBuffer;"); return reinterpret_cast(decoder); } DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs, - jobject jInputBuffer, jint inputSize, jobject jOutputBuffer) { + jobject jInputBuffer, jint inputSize, jobject jOutputBuffer) { OpusMSDecoder* decoder = reinterpret_cast(jDecoder); const uint8_t* inputBuffer = - reinterpret_cast( - env->GetDirectBufferAddress(jInputBuffer)); + reinterpret_cast( + env->GetDirectBufferAddress(jInputBuffer)); const jint outputSize = - kMaxOpusOutputPacketSizeSamples * kBytesPerSample * channelCount; + kMaxOpusOutputPacketSizeSamples * kBytesPerSample * channelCount; env->CallObjectMethod(jOutputBuffer, outputBufferInit, jTimeUs, outputSize); if (env->ExceptionCheck()) { @@ -100,27 +100,27 @@ DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs, return -1; } const jobject jOutputBufferData = env->CallObjectMethod(jOutputBuffer, - outputBufferInit, jTimeUs, outputSize); + outputBufferInit, jTimeUs, outputSize); if (env->ExceptionCheck()) { // Exception is thrown in Java when returning from the native call. return -1; } int16_t* outputBufferData = reinterpret_cast( - env->GetDirectBufferAddress(jOutputBufferData)); + env->GetDirectBufferAddress(jOutputBufferData)); int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize, - outputBufferData, kMaxOpusOutputPacketSizeSamples, 0); + outputBufferData, kMaxOpusOutputPacketSizeSamples, 0); // record error code errorCode = (sampleCount < 0) ? sampleCount : 0; return (sampleCount < 0) ? sampleCount - : sampleCount * kBytesPerSample * channelCount; + : sampleCount * kBytesPerSample * channelCount; } DECODER_FUNC(jint, opusSecureDecode, jlong jDecoder, jlong jTimeUs, - jobject jInputBuffer, jint inputSize, jobject jOutputBuffer, - jint sampleRate, jobject mediaCrypto, jint inputMode, jbyteArray key, - jbyteArray javaIv, jint inputNumSubSamples, jintArray numBytesOfClearData, - jintArray numBytesOfEncryptedData) { + jobject jInputBuffer, jint inputSize, jobject jOutputBuffer, + jint sampleRate, jobject mediaCrypto, jint inputMode, jbyteArray key, + jbyteArray javaIv, jint inputNumSubSamples, jintArray numBytesOfClearData, + jintArray numBytesOfEncryptedData) { // Doesn't support // Java client should have checked vpxSupportSecureDecode // and avoid calling this diff --git a/TMessagesProj/jni/gifvideo.cpp b/TMessagesProj/jni/gifvideo.cpp index 49a900cc7..df137bc03 100644 --- a/TMessagesProj/jni/gifvideo.cpp +++ b/TMessagesProj/jni/gifvideo.cpp @@ -582,18 +582,149 @@ void Java_org_telegram_ui_Components_AnimatedFileDrawable_seekToMs(JNIEnv *env, return; } if (got_frame) { + info->has_decoded_frames = true; + bool finished = false; if (info->frame->format == AV_PIX_FMT_YUV420P || info->frame->format == AV_PIX_FMT_BGRA || info->frame->format == AV_PIX_FMT_YUVJ420P) { int64_t pkt_pts = info->frame->best_effort_timestamp; if (pkt_pts >= pts) { - return; + finished = true; } } av_frame_unref(info->frame); + if (finished) { + return; + } } tries--; } } } + +static inline void writeFrameToBitmap(JNIEnv *env, VideoInfo *info, jintArray data, jobject bitmap, jint stride) { + jint *dataArr = env->GetIntArrayElements(data, 0); + int32_t wantedWidth; + int32_t wantedHeight; + if (dataArr != nullptr) { + wantedWidth = dataArr[0]; + wantedHeight = dataArr[1]; + dataArr[3] = (jint) (1000 * info->frame->best_effort_timestamp * av_q2d(info->video_stream->time_base)); + env->ReleaseIntArrayElements(data, dataArr, 0); + } else { + AndroidBitmapInfo bitmapInfo; + AndroidBitmap_getInfo(env, bitmap, &bitmapInfo); + wantedWidth = bitmapInfo.width; + wantedHeight = bitmapInfo.height; + } + + void *pixels; + if (AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0) { + if (wantedWidth == info->frame->width && wantedHeight == info->frame->height || wantedWidth == info->frame->height && wantedHeight == info->frame->width) { + if (info->sws_ctx == nullptr) { + if (info->frame->format > AV_PIX_FMT_NONE && info->frame->format < AV_PIX_FMT_NB) { + info->sws_ctx = sws_getContext(info->frame->width, info->frame->height, (AVPixelFormat) info->frame->format, info->frame->width, info->frame->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL); + } else if (info->video_dec_ctx->pix_fmt > AV_PIX_FMT_NONE && info->video_dec_ctx->pix_fmt < AV_PIX_FMT_NB) { + info->sws_ctx = sws_getContext(info->video_dec_ctx->width, info->video_dec_ctx->height, info->video_dec_ctx->pix_fmt, info->video_dec_ctx->width, info->video_dec_ctx->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL); + } + } + if (info->sws_ctx == nullptr || ((intptr_t) pixels) % 16 != 0) { + if (info->frame->format == AV_PIX_FMT_YUV420P || info->frame->format == AV_PIX_FMT_YUVJ420P) { + if (info->frame->colorspace == AVColorSpace::AVCOL_SPC_BT709) { + libyuv::H420ToARGB(info->frame->data[0], info->frame->linesize[0], info->frame->data[2], info->frame->linesize[2], info->frame->data[1], info->frame->linesize[1], (uint8_t *) pixels, info->frame->width * 4, info->frame->width, info->frame->height); + } else { + libyuv::I420ToARGB(info->frame->data[0], info->frame->linesize[0], info->frame->data[2], info->frame->linesize[2], info->frame->data[1], info->frame->linesize[1], (uint8_t *) pixels, info->frame->width * 4, info->frame->width, info->frame->height); + } + } else if (info->frame->format == AV_PIX_FMT_BGRA) { + libyuv::ABGRToARGB(info->frame->data[0], info->frame->linesize[0], (uint8_t *) pixels, info->frame->width * 4, info->frame->width, info->frame->height); + } + } else { + info->dst_data[0] = (uint8_t *) pixels; + info->dst_linesize[0] = stride; + sws_scale(info->sws_ctx, info->frame->data, info->frame->linesize, 0, info->frame->height, info->dst_data, info->dst_linesize); + } + } + AndroidBitmap_unlockPixels(env, bitmap); + } +} + +int Java_org_telegram_ui_Components_AnimatedFileDrawable_getFrameAtTime(JNIEnv *env, jclass clazz, jlong ptr, jlong ms, jobject bitmap, jintArray data, jint stride) { + if (ptr == NULL || bitmap == nullptr || data == nullptr) { + return 0; + } + VideoInfo *info = (VideoInfo *) (intptr_t) ptr; + info->seeking = false; + int64_t pts = (int64_t) (ms / av_q2d(info->video_stream->time_base) / 1000); + int ret = 0; + if ((ret = av_seek_frame(info->fmt_ctx, info->video_stream_idx, pts, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME)) < 0) { + LOGE("can't seek file %s, %s", info->src, av_err2str(ret)); + return 0; + } else { + avcodec_flush_buffers(info->video_dec_ctx); + int got_frame = 0; + int32_t tries = 1000; + bool readNextPacket = true; + while (tries > 0) { + if (info->pkt.size == 0 && readNextPacket) { + ret = av_read_frame(info->fmt_ctx, &info->pkt); + if (ret >= 0) { + info->orig_pkt = info->pkt; + } + } + + if (info->pkt.size > 0) { + ret = decode_packet(info, &got_frame); + if (ret < 0) { + if (info->has_decoded_frames) { + ret = 0; + } + info->pkt.size = 0; + } else { + info->pkt.data += ret; + info->pkt.size -= ret; + } + if (info->pkt.size == 0) { + av_packet_unref(&info->orig_pkt); + } + } else { + info->pkt.data = NULL; + info->pkt.size = 0; + ret = decode_packet(info, &got_frame); + if (ret < 0) { + return 0; + } + if (got_frame == 0) { + av_seek_frame(info->fmt_ctx, info->video_stream_idx, 0, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME); + return 0; + } + } + if (ret < 0) { + return 0; + } + if (got_frame) { + bool finished = false; + if (info->frame->format == AV_PIX_FMT_YUV420P || info->frame->format == AV_PIX_FMT_BGRA || info->frame->format == AV_PIX_FMT_YUVJ420P) { + int64_t pkt_pts = info->frame->best_effort_timestamp; + bool isLastPacket = false; + if (info->pkt.size == 0) { + readNextPacket = false; + isLastPacket = av_read_frame(info->fmt_ctx, &info->pkt) < 0; + } + if (pkt_pts >= pts || isLastPacket) { + writeFrameToBitmap(env, info, data, bitmap, stride); + finished = true; + } + } + av_frame_unref(info->frame); + if (finished) { + return 1; + } + } else { + readNextPacket = true; + } + tries--; + } + return 0; + } +} jint Java_org_telegram_ui_Components_AnimatedFileDrawable_getVideoFrame(JNIEnv *env, jclass clazz, jlong ptr, jobject bitmap, jintArray data, jint stride, jboolean preview, jfloat start_time, jfloat end_time) { if (ptr == NULL || bitmap == nullptr) { @@ -617,7 +748,6 @@ jint Java_org_telegram_ui_Components_AnimatedFileDrawable_getVideoFrame(JNIEnv * } else { info->orig_pkt = info->pkt; } - } } @@ -666,55 +796,10 @@ jint Java_org_telegram_ui_Components_AnimatedFileDrawable_getVideoFrame(JNIEnv * if (got_frame) { //LOGD("decoded frame with w = %d, h = %d, format = %d", info->frame->width, info->frame->height, info->frame->format); if (info->frame->format == AV_PIX_FMT_YUV420P || info->frame->format == AV_PIX_FMT_BGRA || info->frame->format == AV_PIX_FMT_YUVJ420P) { - jint *dataArr = env->GetIntArrayElements(data, 0); - int32_t wantedWidth; - int32_t wantedHeight; - if (dataArr != nullptr) { - wantedWidth = dataArr[0]; - wantedHeight = dataArr[1]; - dataArr[3] = (jint) (1000 * info->frame->best_effort_timestamp * av_q2d(info->video_stream->time_base)); - env->ReleaseIntArrayElements(data, dataArr, 0); - } else { - AndroidBitmapInfo bitmapInfo; - AndroidBitmap_getInfo(env, bitmap, &bitmapInfo); - wantedWidth = bitmapInfo.width; - wantedHeight = bitmapInfo.height; - } - - void *pixels; - if (AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0) { - if (wantedWidth == info->frame->width && wantedHeight == info->frame->height || wantedWidth == info->frame->height && wantedHeight == info->frame->width) { - if (info->sws_ctx == nullptr) { - if (info->frame->format > AV_PIX_FMT_NONE && info->frame->format < AV_PIX_FMT_NB) { - info->sws_ctx = sws_getContext(info->frame->width, info->frame->height, (AVPixelFormat) info->frame->format, info->frame->width, info->frame->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL); - } else if (info->video_dec_ctx->pix_fmt > AV_PIX_FMT_NONE && info->video_dec_ctx->pix_fmt < AV_PIX_FMT_NB) { - info->sws_ctx = sws_getContext(info->video_dec_ctx->width, info->video_dec_ctx->height, info->video_dec_ctx->pix_fmt, info->video_dec_ctx->width, info->video_dec_ctx->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL); - } - } - if (info->sws_ctx == nullptr || ((intptr_t) pixels) % 16 != 0) { - if (info->frame->format == AV_PIX_FMT_YUV420P || info->frame->format == AV_PIX_FMT_YUVJ420P) { - if (info->frame->colorspace == AVColorSpace::AVCOL_SPC_BT709) { - libyuv::H420ToARGB(info->frame->data[0], info->frame->linesize[0], info->frame->data[2], info->frame->linesize[2], info->frame->data[1], info->frame->linesize[1], (uint8_t *) pixels, info->frame->width * 4, info->frame->width, info->frame->height); - } else { - libyuv::I420ToARGB(info->frame->data[0], info->frame->linesize[0], info->frame->data[2], info->frame->linesize[2], info->frame->data[1], info->frame->linesize[1], (uint8_t *) pixels, info->frame->width * 4, info->frame->width, info->frame->height); - } - } else if (info->frame->format == AV_PIX_FMT_BGRA) { - libyuv::ABGRToARGB(info->frame->data[0], info->frame->linesize[0], (uint8_t *) pixels, info->frame->width * 4, info->frame->width, info->frame->height); - } - } else { - info->dst_data[0] = (uint8_t *) pixels; - info->dst_linesize[0] = stride; - sws_scale(info->sws_ctx, info->frame->data, info->frame->linesize, 0, info->frame->height, info->dst_data, info->dst_linesize); - } - } - AndroidBitmap_unlockPixels(env, bitmap); - } + writeFrameToBitmap(env, info, data, bitmap, stride); } info->has_decoded_frames = true; av_frame_unref(info->frame); - - //LOGD("frame time %lld ms", ConnectionsManager::getInstance(0).getCurrentTimeMonotonicMillis() - time); - return 1; } if (!info->has_decoded_frames) { diff --git a/TMessagesProj/jni/rlottie/src/lottie/lottieparser.cpp b/TMessagesProj/jni/rlottie/src/lottie/lottieparser.cpp index f3d434623..cf7222145 100755 --- a/TMessagesProj/jni/rlottie/src/lottie/lottieparser.cpp +++ b/TMessagesProj/jni/rlottie/src/lottie/lottieparser.cpp @@ -822,8 +822,10 @@ void LottieParserImpl::parseLayers(LOTCompositionData *comp) { return; } std::shared_ptr layer = parseLayer(true); - staticFlag = staticFlag && layer->isStatic(); - comp->mRootLayer->mChildren.push_back(layer); + if (layer) { + staticFlag = staticFlag && layer->isStatic(); + comp->mRootLayer->mChildren.push_back(layer); + } } if (!IsValid()) { parsingError = true; diff --git a/TMessagesProj/jni/tgnet/ApiScheme.cpp b/TMessagesProj/jni/tgnet/ApiScheme.cpp index e8d7049dd..62c23955a 100644 --- a/TMessagesProj/jni/tgnet/ApiScheme.cpp +++ b/TMessagesProj/jni/tgnet/ApiScheme.cpp @@ -1172,7 +1172,7 @@ UserProfilePhoto *UserProfilePhoto::TLdeserialize(NativeByteBuffer *stream, uint case 0x4f11bae1: result = new TL_userProfilePhotoEmpty(); break; - case 0xecd75d8c: + case 0x69d3ab26: result = new TL_userProfilePhoto(); break; default: @@ -1189,6 +1189,8 @@ void TL_userProfilePhotoEmpty::serializeToStream(NativeByteBuffer *stream) { } void TL_userProfilePhoto::readParams(NativeByteBuffer *stream, int32_t instanceNum, bool &error) { + flags = stream->readInt32(&error); + has_video = (flags & 1) != 0; photo_id = stream->readInt64(&error); photo_small = std::unique_ptr(FileLocation::TLdeserialize(stream, stream->readUint32(&error), instanceNum, error)); photo_big = std::unique_ptr(FileLocation::TLdeserialize(stream, stream->readUint32(&error), instanceNum, error)); @@ -1197,6 +1199,8 @@ void TL_userProfilePhoto::readParams(NativeByteBuffer *stream, int32_t instanceN void TL_userProfilePhoto::serializeToStream(NativeByteBuffer *stream) { stream->writeInt32(constructor); + flags = has_video ? (flags | 1) : (flags &~ 1); + stream->writeInt32(flags); stream->writeInt64(photo_id); photo_small->serializeToStream(stream); photo_big->serializeToStream(stream); diff --git a/TMessagesProj/jni/tgnet/ApiScheme.h b/TMessagesProj/jni/tgnet/ApiScheme.h index ee26d11ce..330579d37 100644 --- a/TMessagesProj/jni/tgnet/ApiScheme.h +++ b/TMessagesProj/jni/tgnet/ApiScheme.h @@ -252,6 +252,8 @@ public: class UserProfilePhoto : public TLObject { public: + int32_t flags; + bool has_video; int64_t photo_id; std::unique_ptr photo_small; std::unique_ptr photo_big; @@ -271,7 +273,7 @@ public: class TL_userProfilePhoto : public UserProfilePhoto { public: - static const uint32_t constructor = 0xecd75d8c; + static const uint32_t constructor = 0x69d3ab26; void readParams(NativeByteBuffer *stream, int32_t instanceNum, bool &error); void serializeToStream(NativeByteBuffer *stream); diff --git a/TMessagesProj/jni/tgnet/ConnectionSocket.cpp b/TMessagesProj/jni/tgnet/ConnectionSocket.cpp index 339f7c86a..1c53b34bb 100644 --- a/TMessagesProj/jni/tgnet/ConnectionSocket.cpp +++ b/TMessagesProj/jni/tgnet/ConnectionSocket.cpp @@ -210,10 +210,10 @@ public: Op::zero(32), Op::string("\x20", 1), Op::random(32), - Op::string("\x00\x22", 2), + Op::string("\x00\x20", 2), Op::grease(0), Op::string("\x13\x01\x13\x02\x13\x03\xc0\x2b\xc0\x2f\xc0\x2c\xc0\x30\xcc\xa9\xcc\xa8\xc0\x13\xc0\x14\x00\x9c" - "\x00\x9d\x00\x2f\x00\x35\x00\x0a\x01\x00\x01\x91", 36), + "\x00\x9d\x00\x2f\x00\x35\x01\x00\x01\x93", 34), Op::grease(2), Op::string("\x00\x00\x00\x00", 4), Op::begin_scope(), @@ -228,8 +228,8 @@ public: Op::grease(4), Op::string( "\x00\x1d\x00\x17\x00\x18\x00\x0b\x00\x02\x01\x00\x00\x23\x00\x00\x00\x10\x00\x0e\x00\x0c\x02\x68\x32\x08" - "\x68\x74\x74\x70\x2f\x31\x2e\x31\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x0d\x00\x14\x00\x12\x04\x03\x08" - "\x04\x04\x01\x05\x03\x08\x05\x05\x01\x08\x06\x06\x01\x02\x01\x00\x12\x00\x00\x00\x33\x00\x2b\x00\x29", 77), + "\x68\x74\x74\x70\x2f\x31\x2e\x31\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x0d\x00\x12\x00\x10\x04\x03\x08" + "\x04\x04\x01\x05\x03\x08\x05\x05\x01\x08\x06\x06\x01\x00\x12\x00\x00\x00\x33\x00\x2b\x00\x29", 75), Op::grease(4), Op::string("\x00\x01\x00\x00\x1d\x00\x20", 7), Op::K(), diff --git a/TMessagesProj/jni/tgnet/ConnectionsManager.cpp b/TMessagesProj/jni/tgnet/ConnectionsManager.cpp index 9e5979bd3..41e06eb4e 100644 --- a/TMessagesProj/jni/tgnet/ConnectionsManager.cpp +++ b/TMessagesProj/jni/tgnet/ConnectionsManager.cpp @@ -2748,6 +2748,13 @@ std::unique_ptr ConnectionsManager::wrapInLayer(TLObject *object, Data TL_jsonObjectValue *objectValue = new TL_jsonObjectValue(); jsonObject->value.push_back(std::unique_ptr(objectValue)); + TL_jsonString *jsonString = new TL_jsonString(); + jsonString->value = installer; + objectValue->key = "installer"; + objectValue->value = std::unique_ptr(jsonString); + + objectValue = new TL_jsonObjectValue(); + jsonObject->value.push_back(std::unique_ptr(objectValue)); TL_jsonNumber *jsonNumber = new TL_jsonNumber(); jsonNumber->value = currentDeviceTimezone; @@ -3271,7 +3278,7 @@ void ConnectionsManager::applyDnsConfig(NativeByteBuffer *buffer, std::string ph }); } -void ConnectionsManager::init(uint32_t version, int32_t layer, int32_t apiId, std::string deviceModel, std::string systemVersion, std::string appVersion, std::string langCode, std::string systemLangCode, std::string configPath, std::string logPath, std::string regId, std::string cFingerpting, int32_t timezoneOffset, int32_t userId, bool isPaused, bool enablePushConnection, bool hasNetwork, int32_t networkType) { +void ConnectionsManager::init(uint32_t version, int32_t layer, int32_t apiId, std::string deviceModel, std::string systemVersion, std::string appVersion, std::string langCode, std::string systemLangCode, std::string configPath, std::string logPath, std::string regId, std::string cFingerpting, std::string installerId, int32_t timezoneOffset, int32_t userId, bool isPaused, bool enablePushConnection, bool hasNetwork, int32_t networkType) { currentVersion = version; currentLayer = layer; currentApiId = apiId; @@ -3282,6 +3289,7 @@ void ConnectionsManager::init(uint32_t version, int32_t layer, int32_t apiId, st currentLangCode = langCode; currentRegId = regId; certFingerprint = cFingerpting; + installer = installerId; currentDeviceTimezone = timezoneOffset; currentSystemLangCode = systemLangCode; currentUserId = userId; diff --git a/TMessagesProj/jni/tgnet/ConnectionsManager.h b/TMessagesProj/jni/tgnet/ConnectionsManager.h index 0965e2f71..a3635b31f 100644 --- a/TMessagesProj/jni/tgnet/ConnectionsManager.h +++ b/TMessagesProj/jni/tgnet/ConnectionsManager.h @@ -66,7 +66,7 @@ public: void pauseNetwork(); void setNetworkAvailable(bool value, int32_t type, bool slow); void setUseIpv6(bool value); - void init(uint32_t version, int32_t layer, int32_t apiId, std::string deviceModel, std::string systemVersion, std::string appVersion, std::string langCode, std::string systemLangCode, std::string configPath, std::string logPath, std::string regId, std::string cFingerprint, int32_t timezoneOffset, int32_t userId, bool isPaused, bool enablePushConnection, bool hasNetwork, int32_t networkType); + void init(uint32_t version, int32_t layer, int32_t apiId, std::string deviceModel, std::string systemVersion, std::string appVersion, std::string langCode, std::string systemLangCode, std::string configPath, std::string logPath, std::string regId, std::string cFingerprint, std::string installerId, int32_t timezoneOffset, int32_t userId, bool isPaused, bool enablePushConnection, bool hasNetwork, int32_t networkType); void setProxySettings(std::string address, uint16_t port, std::string username, std::string password, std::string secret); void setLangCode(std::string langCode); void setRegId(std::string regId); @@ -218,6 +218,7 @@ private: std::string currentLangCode; std::string currentRegId; std::string certFingerprint; + std::string installer; int32_t currentDeviceTimezone = 0; std::string currentSystemLangCode; std::string currentConfigPath; diff --git a/TMessagesProj/proguard-rules.pro b/TMessagesProj/proguard-rules.pro index 0968602e7..6032c8ce4 100644 --- a/TMessagesProj/proguard-rules.pro +++ b/TMessagesProj/proguard-rules.pro @@ -57,6 +57,74 @@ java.lang.Object readResolve(); } + +# Constant folding for resource integers may mean that a resource passed to this method appears to be unused. Keep the method to prevent this from happening. +-keep class com.google.android.exoplayer2.upstream.RawResourceDataSource { + public static android.net.Uri buildRawResourceUri(int); +} + +# Methods accessed via reflection in DefaultExtractorsFactory +-dontnote com.google.android.exoplayer2.ext.flac.FlacLibrary +-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacLibrary { + +} + +# Some members of this class are being accessed from native methods. Keep them unobfuscated. +-keep class com.google.android.exoplayer2.video.VideoDecoderOutputBuffer { + *; +} + +-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer +-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer { + (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]); +} +-dontnote com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer +-keepclassmembers class com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer { + (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]); +} +-dontnote com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer +-keepclassmembers class com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer { + (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]); +} + +# Constructors accessed via reflection in DefaultExtractorsFactory +-dontnote com.google.android.exoplayer2.ext.flac.FlacExtractor +-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacExtractor { + (); +} + +# Constructors accessed via reflection in DefaultDownloaderFactory +-dontnote com.google.android.exoplayer2.source.dash.offline.DashDownloader +-keepclassmembers class com.google.android.exoplayer2.source.dash.offline.DashDownloader { + (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper); +} +-dontnote com.google.android.exoplayer2.source.hls.offline.HlsDownloader +-keepclassmembers class com.google.android.exoplayer2.source.hls.offline.HlsDownloader { + (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper); +} +-dontnote com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader +-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloader { + (android.net.Uri, java.util.List, com.google.android.exoplayer2.offline.DownloaderConstructorHelper); +} + +# Constructors accessed via reflection in DownloadHelper +-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory +-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { + (com.google.android.exoplayer2.upstream.DataSource$Factory); +} +-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory +-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { + (com.google.android.exoplayer2.upstream.DataSource$Factory); +} +-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory +-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { + (com.google.android.exoplayer2.upstream.DataSource$Factory); +} + +# Don't warn about checkerframework and Kotlin annotations +-dontwarn org.checkerframework.** +-dontwarn javax.annotation.** + # Use -keep to explicitly keep any other classes shrinking would remove # -dontoptimize -dontobfuscate diff --git a/TMessagesProj/src/main/AndroidManifest.xml b/TMessagesProj/src/main/AndroidManifest.xml index 2d8749ab2..56952dc6c 100644 --- a/TMessagesProj/src/main/AndroidManifest.xml +++ b/TMessagesProj/src/main/AndroidManifest.xml @@ -217,11 +217,17 @@ - - + + + - - - - - - runOnAnimationsEnd = new ArrayList<>(); private boolean shouldAnimateEnterFromBottom; + private RecyclerView.ViewHolder greetingsSticker; + private ChatGreetingsView chatGreetingsView; public ChatListItemAnimator(ChatActivity activity, RecyclerListView listView) { this.activity = activity; @@ -52,6 +63,14 @@ public class ChatListItemAnimator extends DefaultItemAnimator { @Override public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + return; + } + boolean runTranslationFromBottom = false; if (shouldAnimateEnterFromBottom) { for (int i = 0; i < mPendingAdditions.size(); i++) { @@ -201,7 +220,7 @@ public class ChatListItemAnimator extends DefaultItemAnimator { @Override public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { boolean res = super.animateAppearance(viewHolder, preLayoutInfo, postLayoutInfo); - if (res) { + if (res && shouldAnimateEnterFromBottom) { boolean runTranslationFromBottom = false; for (int i = 0; i < mPendingAdditions.size(); i++) { if (mPendingAdditions.get(i).getLayoutPosition() == 0) { @@ -532,13 +551,7 @@ public class ChatListItemAnimator extends DefaultItemAnimator { } } else if (holder.itemView instanceof BotHelpCell) { BotHelpCell botInfo = (BotHelpCell) holder.itemView; - if (!botInfo.wasDraw) { - dispatchMoveFinished(holder); - botInfo.setAnimating(false); - return false; - } else { - botInfo.setAnimating(true); - } + botInfo.setAnimating(true); } else { if (deltaX == 0 && deltaY == 0) { dispatchMoveFinished(holder); @@ -666,9 +679,13 @@ public class ChatListItemAnimator extends DefaultItemAnimator { animatorSet.playTogether(valueAnimator); } + MessageObject.GroupedMessages group = chatMessageCell.getCurrentMessagesGroup(); + if (group == null) { + moveInfoExtended.animateChangeGroupBackground = false; + } + if (moveInfoExtended.animateChangeGroupBackground) { ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 0); - MessageObject.GroupedMessages group = chatMessageCell.getCurrentMessagesGroup(); MessageObject.GroupedMessages.TransitionParams groupTransitionParams = group.transitionParams; RecyclerListView recyclerListView = (RecyclerListView) holder.itemView.getParent(); @@ -738,10 +755,12 @@ public class ChatListItemAnimator extends DefaultItemAnimator { @Override public void onAnimationEnd(Animator animator) { animator.removeAllListeners(); + restoreTransitionParams(holder.itemView); if (holder.itemView instanceof ChatMessageCell) { - ((ChatMessageCell) holder.itemView).getTransitionParams().resetAnimation(); - } else if (holder.itemView instanceof BotHelpCell) { - ((BotHelpCell) holder.itemView).setAnimating(false); + MessageObject.GroupedMessages group = ((ChatMessageCell) view).getCurrentMessagesGroup(); + if (group != null) { + group.transitionParams.reset(); + } } if (mMoveAnimations.remove(holder)) { dispatchMoveFinished(holder); @@ -935,11 +954,32 @@ public class ChatListItemAnimator extends DefaultItemAnimator { animator.cancel(); } super.endAnimation(item); + restoreTransitionParams(item.itemView); if (BuildVars.LOGS_ENABLED) { FileLog.d("end animation"); } } + private void restoreTransitionParams(View view) { + view.setAlpha(1f); + view.setTranslationY(0f); + if (view instanceof BotHelpCell) { + BotHelpCell botCell = (BotHelpCell) view; + int top = recyclerListView.getMeasuredHeight() / 2 - view.getMeasuredHeight() / 2; + botCell.setAnimating(false); + if (view.getTop() > top) { + view.setTranslationY(top - view.getTop()); + } else { + view.setTranslationY(0); + } + } else if (view instanceof ChatMessageCell) { + ((ChatMessageCell) view).getTransitionParams().resetAnimation(); + ((ChatMessageCell) view).setAnimationOffsetX(0f); + } else { + view.setTranslationX(0f); + } + } + @Override public void endAnimations() { if (BuildVars.LOGS_ENABLED) { @@ -949,39 +989,33 @@ public class ChatListItemAnimator extends DefaultItemAnimator { groupedMessages.transitionParams.isNewGroup = false; } willChangedGroups.clear(); - cancelAnimators(); + if (chatGreetingsView != null) { + chatGreetingsView.stickerToSendView.setAlpha(1f); + } + greetingsSticker = null; + chatGreetingsView = null; + int count = mPendingMoves.size(); for (int i = count - 1; i >= 0; i--) { MoveInfo item = mPendingMoves.get(i); View view = item.holder.itemView; - view.setTranslationY(0); - if (view instanceof ChatMessageCell) { - ChatMessageCell cell = ((ChatMessageCell) view); - ChatMessageCell.TransitionParams params = cell.getTransitionParams(); - cell.setAnimationOffsetX(0); - params.deltaLeft = 0; - params.deltaTop = 0; - params.deltaRight = 0; - params.deltaBottom = 0; - params.animateBackgroundBoundsInner = false; - } else { - view.setTranslationX(0); - } + restoreTransitionParams(view); dispatchMoveFinished(item.holder); mPendingMoves.remove(i); } count = mPendingRemovals.size(); for (int i = count - 1; i >= 0; i--) { RecyclerView.ViewHolder item = mPendingRemovals.get(i); + restoreTransitionParams(item.itemView); dispatchRemoveFinished(item); mPendingRemovals.remove(i); } count = mPendingAdditions.size(); for (int i = count - 1; i >= 0; i--) { RecyclerView.ViewHolder item = mPendingAdditions.get(i); - item.itemView.setAlpha(1); + restoreTransitionParams(item.itemView); dispatchAddFinished(item); mPendingAdditions.remove(i); } @@ -1001,13 +1035,7 @@ public class ChatListItemAnimator extends DefaultItemAnimator { for (int j = count - 1; j >= 0; j--) { MoveInfo moveInfo = moves.get(j); RecyclerView.ViewHolder item = moveInfo.holder; - View view = item.itemView; - view.setTranslationY(0); - if (view instanceof ChatMessageCell) { - ((ChatMessageCell) view).setAnimationOffsetX(0); - } else { - view.setTranslationX(0); - } + restoreTransitionParams(item.itemView); dispatchMoveFinished(moveInfo.holder); moves.remove(j); if (moves.isEmpty()) { @@ -1021,8 +1049,7 @@ public class ChatListItemAnimator extends DefaultItemAnimator { count = additions.size(); for (int j = count - 1; j >= 0; j--) { RecyclerView.ViewHolder item = additions.get(j); - View view = item.itemView; - view.setAlpha(1); + restoreTransitionParams(item.itemView); dispatchAddFinished(item); additions.remove(j); if (additions.isEmpty()) { @@ -1067,14 +1094,7 @@ public class ChatListItemAnimator extends DefaultItemAnimator { } else { return false; } - item.itemView.setAlpha(1); - if (item.itemView instanceof ChatMessageCell) { - ((ChatMessageCell) item.itemView).setAnimationOffsetX(0); - ((ChatMessageCell) item.itemView).getTransitionParams().resetAnimation(); - } else { - item.itemView.setTranslationX(0); - } - item.itemView.setTranslationY(0); + restoreTransitionParams(item.itemView); dispatchChangeFinished(item, oldItem); return true; @@ -1120,6 +1140,9 @@ public class ChatListItemAnimator extends DefaultItemAnimator { } final View view = holder.itemView; mAddAnimations.add(holder); + if (holder == greetingsSticker) { + view.setAlpha(1f); + } AnimatorSet animatorSet = new AnimatorSet(); ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.ALPHA, view.getAlpha(), 1f); @@ -1136,14 +1159,76 @@ public class ChatListItemAnimator extends DefaultItemAnimator { } else { view.animate().translationX(0).translationY(0).setDuration(getAddDuration()).start(); } - animatorSet.setDuration(getAddDuration()); + if (view instanceof ChatMessageCell){ - MessageObject.GroupedMessages groupedMessages = ((ChatMessageCell) view).getCurrentMessagesGroup(); - if (groupedMessages != null && groupedMessages.transitionParams.backgroundChangeBounds) { - animatorSet.setStartDelay(140); + if (holder == greetingsSticker) { + if (chatGreetingsView != null) { + chatGreetingsView.stickerToSendView.setAlpha(0f); + } + recyclerListView.setClipChildren(false); + ChatMessageCell messageCell = (ChatMessageCell) view; + View parentForGreetingsView = (View)chatGreetingsView.getParent(); + float fromX = chatGreetingsView.stickerToSendView.getX() + chatGreetingsView.getX() + parentForGreetingsView.getX(); + float fromY = chatGreetingsView.stickerToSendView.getY() + chatGreetingsView.getY() + parentForGreetingsView.getY(); + float toX = messageCell.getPhotoImage().getImageX() + recyclerListView.getX() + messageCell.getX(); + float toY = messageCell.getPhotoImage().getImageY() + recyclerListView.getY() + messageCell.getY(); + float fromW = chatGreetingsView.stickerToSendView.getWidth(); + float fromH = chatGreetingsView.stickerToSendView.getHeight(); + float toW = messageCell.getPhotoImage().getImageWidth(); + float toH = messageCell.getPhotoImage().getImageHeight(); + float deltaX = fromX - toX; + float deltaY = fromY - toY; + + toX = messageCell.getPhotoImage().getImageX(); + toY = messageCell.getPhotoImage().getImageY(); + + messageCell.getTransitionParams().imageChangeBoundsTransition = true; + messageCell.getTransitionParams().animateDrawingTimeAlpha = true; + messageCell.getPhotoImage().setImageCoords(toX + deltaX, toX + deltaY, fromW,fromH); + + ValueAnimator valueAnimator = ValueAnimator.ofFloat(0,1f); + float finalToX = toX; + float finalToY = toY; + valueAnimator.addUpdateListener(animation -> { + float v = (float) animation.getAnimatedValue(); + messageCell.getTransitionParams().animateChangeProgress = v; + if (messageCell.getTransitionParams().animateChangeProgress > 1) { + messageCell.getTransitionParams().animateChangeProgress = 1f; + } + messageCell.getPhotoImage().setImageCoords( + finalToX + deltaX * (1f - v), + finalToY + deltaY * (1f - v), + fromW * (1f - v) + toW * v, + fromH * (1f - v) + toH * v); + messageCell.invalidate(); + }); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + messageCell.getTransitionParams().resetAnimation(); + messageCell.getPhotoImage().setImageCoords(finalToX, finalToY, toW, toH); + if (chatGreetingsView != null) { + chatGreetingsView.stickerToSendView.setAlpha(1f); + } + messageCell.invalidate(); + } + }); + animatorSet.play(valueAnimator); + } else { + MessageObject.GroupedMessages groupedMessages = ((ChatMessageCell) view).getCurrentMessagesGroup(); + if (groupedMessages != null && groupedMessages.transitionParams.backgroundChangeBounds) { + animatorSet.setStartDelay(140); + } } } + if (holder == greetingsSticker) { + animatorSet.setDuration(350); + animatorSet.setInterpolator(new OvershootInterpolator()); + } else { + animatorSet.setDuration(getAddDuration()); + } + animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animator) { @@ -1232,6 +1317,31 @@ public class ChatListItemAnimator extends DefaultItemAnimator { onAllAnimationsDone(); } + public boolean willRemoved(View view) { + RecyclerView.ViewHolder holder = recyclerListView.getChildViewHolder(view); + if (holder != null) { + return mPendingRemovals.contains(holder) || mRemoveAnimations.contains(holder); + } + return false; + } + + public boolean willAddedFromAlpha(View view) { + if (shouldAnimateEnterFromBottom) { + return false; + } + RecyclerView.ViewHolder holder = recyclerListView.getChildViewHolder(view); + if (holder != null) { + return mPendingAdditions.contains(holder) || mAddAnimations.contains(holder); + } + return false; + } + + public void onGreetingStickerTransition(RecyclerView.ViewHolder holder, ChatGreetingsView greetingsViewContainer) { + greetingsSticker = holder; + chatGreetingsView = greetingsViewContainer; + shouldAnimateEnterFromBottom = false; + } + class MoveInfoExtended extends MoveInfo { public float captionDeltaX; diff --git a/TMessagesProj/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java b/TMessagesProj/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java index caf456126..1327423f8 100644 --- a/TMessagesProj/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java +++ b/TMessagesProj/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java @@ -30,7 +30,6 @@ import androidx.core.view.ViewCompat; import org.telegram.messenger.BuildVars; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; /** @@ -186,7 +185,7 @@ public class DefaultItemAnimator extends SimpleItemAnimator { long removeDuration = removalsPending ? getRemoveDuration() : 0; long moveDuration = movesPending ? getMoveDuration() : 0; long changeDuration = changesPending ? getChangeDuration() : 0; - long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + long totalDelay = getAddAnimationDelay(removeDuration,moveDuration,changeDuration); View view = additions.get(0).itemView; ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); } else { @@ -195,6 +194,10 @@ public class DefaultItemAnimator extends SimpleItemAnimator { } } + protected long getAddAnimationDelay(long removeDuration, long moveDuration, long changeDuration) { + return removeDuration + Math.max(moveDuration, changeDuration); + } + protected long getMoveAnimationDelay() { return getRemoveDuration(); } diff --git a/TMessagesProj/src/main/java/androidx/recyclerview/widget/GapWorker.java b/TMessagesProj/src/main/java/androidx/recyclerview/widget/GapWorker.java index b2d9f2e77..0ac777d20 100644 --- a/TMessagesProj/src/main/java/androidx/recyclerview/widget/GapWorker.java +++ b/TMessagesProj/src/main/java/androidx/recyclerview/widget/GapWorker.java @@ -158,7 +158,7 @@ final class GapWorker implements Runnable { public void add(RecyclerView recyclerView) { if (RecyclerView.DEBUG && mRecyclerViews.contains(recyclerView)) { - throw new IllegalStateException("RecyclerView already present in worker list!"); + return; } mRecyclerViews.add(recyclerView); } diff --git a/TMessagesProj/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/TMessagesProj/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java index 93a735d3f..5f50bb49c 100644 --- a/TMessagesProj/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java +++ b/TMessagesProj/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java @@ -25,9 +25,6 @@ import android.view.ViewGroup; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; -import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.BuildVars; - import java.util.Arrays; /** @@ -38,7 +35,7 @@ import java.util.Arrays; */ public class GridLayoutManager extends LinearLayoutManager { - private static final boolean DEBUG = BuildVars.DEBUG_VERSION; + private static final boolean DEBUG = false; private static final String TAG = "GridLayoutManager"; public static final int DEFAULT_SPAN_COUNT = -1; /** diff --git a/TMessagesProj/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/TMessagesProj/src/main/java/androidx/recyclerview/widget/RecyclerView.java index b78453f22..881135d97 100644 --- a/TMessagesProj/src/main/java/androidx/recyclerview/widget/RecyclerView.java +++ b/TMessagesProj/src/main/java/androidx/recyclerview/widget/RecyclerView.java @@ -4099,48 +4099,60 @@ public class RecyclerView extends ViewGroup implements ScrollingView, // Step 3: Find out where things are now, and process change animations. // traverse list in reverse because we may call animateChange in the loop which may // remove the target view holder. - for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { - ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); - if (holder.shouldIgnore()) { - continue; - } - long key = getChangedHolderKey(holder); - final ItemHolderInfo animationInfo = mItemAnimator - .recordPostLayoutInformation(mState, holder); - ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); - if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) { - // run a change animation - - // If an Item is CHANGED but the updated version is disappearing, it creates - // a conflicting case. - // Since a view that is marked as disappearing is likely to be going out of - // bounds, we run a change animation. Both views will be cleaned automatically - // once their animations finish. - // On the other hand, if it is the same view holder instance, we run a - // disappearing animation instead because we are not going to rebind the updated - // VH unless it is enforced by the layout manager. - final boolean oldDisappearing = mViewInfoStore.isDisappearing( - oldChangeViewHolder); - final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); - if (oldDisappearing && oldChangeViewHolder == holder) { - // run disappear animation instead of change - mViewInfoStore.addToPostLayout(holder, animationInfo); - } else { - final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( - oldChangeViewHolder); - // we add and remove so that any post info is merged. - mViewInfoStore.addToPostLayout(holder, animationInfo); - ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); - if (preInfo == null) { - handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder); - } else { - animateChange(oldChangeViewHolder, holder, preInfo, postInfo, - oldDisappearing, newDisappearing); - } + try { + for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { + ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore()) { + continue; + } + long key = getChangedHolderKey(holder); + final ItemHolderInfo animationInfo = mItemAnimator + .recordPostLayoutInformation(mState, holder); + ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); + if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) { + // run a change animation + + // If an Item is CHANGED but the updated version is disappearing, it creates + // a conflicting case. + // Since a view that is marked as disappearing is likely to be going out of + // bounds, we run a change animation. Both views will be cleaned automatically + // once their animations finish. + // On the other hand, if it is the same view holder instance, we run a + // disappearing animation instead because we are not going to rebind the updated + // VH unless it is enforced by the layout manager. + final boolean oldDisappearing = mViewInfoStore.isDisappearing( + oldChangeViewHolder); + final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); + if (oldDisappearing && oldChangeViewHolder == holder) { + // run disappear animation instead of change + mViewInfoStore.addToPostLayout(holder, animationInfo); + } else { + final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( + oldChangeViewHolder); + // we add and remove so that any post info is merged. + mViewInfoStore.addToPostLayout(holder, animationInfo); + ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); + if (preInfo == null) { + handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder); + } else { + animateChange(oldChangeViewHolder, holder, preInfo, postInfo, + oldDisappearing, newDisappearing); + } + } + } else { + mViewInfoStore.addToPostLayout(holder, animationInfo); } - } else { - mViewInfoStore.addToPostLayout(holder, animationInfo); } + } catch (Exception e) { + StringBuilder builder = new StringBuilder(); + for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { + ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore()) { + continue; + } + builder.append("Holder at" + i + " " + holder + "\n"); + } + throw new RuntimeException(builder.toString(), e); } // Step 4: Process view info lists and trigger animations @@ -11511,7 +11523,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, * to store any additional required per-child view metadata about the layout. */ public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams { - ViewHolder mViewHolder; + public ViewHolder mViewHolder; public final Rect mDecorInsets = new Rect(); boolean mInsetsDirty = true; // Flag is set to true if the view is bound while it is detached from RV. diff --git a/TMessagesProj/src/main/java/com/google/android/README.md b/TMessagesProj/src/main/java/com/google/android/README.md index 166fab549..e5bc44002 100644 --- a/TMessagesProj/src/main/java/com/google/android/README.md +++ b/TMessagesProj/src/main/java/com/google/android/README.md @@ -1,5 +1,8 @@ change SimpleExoPlayer.java +change Player.java change VideoListener.java change AspectRatioFrameLayout.java change DefaultExtractorsFactory.java +change MediaCodecVideoRenderer.java +add SurfaceNotValidException.java change MP4Extractor.java - MAXIMUM_READ_AHEAD_BYTES_STREAM to 1MB diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/AudioBecomingNoisyManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/AudioBecomingNoisyManager.java new file mode 100644 index 000000000..2a52a039d --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/AudioBecomingNoisyManager.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.os.Handler; + +/* package */ final class AudioBecomingNoisyManager { + + private final Context context; + private final AudioBecomingNoisyReceiver receiver; + private boolean receiverRegistered; + + public interface EventListener { + void onAudioBecomingNoisy(); + } + + public AudioBecomingNoisyManager(Context context, Handler eventHandler, EventListener listener) { + this.context = context.getApplicationContext(); + this.receiver = new AudioBecomingNoisyReceiver(eventHandler, listener); + } + + /** + * Enables the {@link AudioBecomingNoisyManager} which calls {@link + * EventListener#onAudioBecomingNoisy()} upon receiving an intent of {@link + * AudioManager#ACTION_AUDIO_BECOMING_NOISY}. + * + * @param enabled True if the listener should be notified when audio is becoming noisy. + */ + public void setEnabled(boolean enabled) { + if (enabled && !receiverRegistered) { + context.registerReceiver( + receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + receiverRegistered = true; + } else if (!enabled && receiverRegistered) { + context.unregisterReceiver(receiver); + receiverRegistered = false; + } + } + + private final class AudioBecomingNoisyReceiver extends BroadcastReceiver implements Runnable { + private final EventListener listener; + private final Handler eventHandler; + + public AudioBecomingNoisyReceiver(Handler eventHandler, EventListener listener) { + this.eventHandler = eventHandler; + this.listener = listener; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { + eventHandler.post(this); + } + } + + @Override + public void run() { + if (receiverRegistered) { + listener.onAudioBecomingNoisy(); + } + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java similarity index 64% rename from TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java rename to TMessagesProj/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java index 3cc05e87d..5aeca440f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioFocusManager.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/AudioFocusManager.java @@ -13,19 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.audio; +package com.google.android.exoplayer2; import android.content.Context; import android.media.AudioFocusRequest; import android.media.AudioManager; +import android.os.Handler; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -35,7 +33,7 @@ import java.lang.annotation.RetentionPolicy; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Manages requesting and responding to changes in audio focus. */ -public final class AudioFocusManager { +/* package */ final class AudioFocusManager { /** Interface to allow AudioFocusManager to give commands to a player. */ public interface PlayerControl { @@ -77,15 +75,12 @@ public final class AudioFocusManager { @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ - AUDIO_FOCUS_STATE_LOST_FOCUS, AUDIO_FOCUS_STATE_NO_FOCUS, AUDIO_FOCUS_STATE_HAVE_FOCUS, AUDIO_FOCUS_STATE_LOSS_TRANSIENT, AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK }) private @interface AudioFocusState {} - /** No audio focus was held, but has been lost by another app taking it permanently. */ - private static final int AUDIO_FOCUS_STATE_LOST_FOCUS = -1; /** No audio focus is currently being held. */ private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0; /** The requested audio focus is currently held. */ @@ -102,12 +97,12 @@ public final class AudioFocusManager { private final AudioManager audioManager; private final AudioFocusListener focusListener; - private final PlayerControl playerControl; - private @Nullable AudioAttributes audioAttributes; + @Nullable private PlayerControl playerControl; + @Nullable private AudioAttributes audioAttributes; - private @AudioFocusState int audioFocusState; - private int focusGain; - private float volumeMultiplier = 1.0f; + @AudioFocusState private int audioFocusState; + @C.AudioFocusGain private int focusGain; + private float volumeMultiplier = VOLUME_MULTIPLIER_DEFAULT; private @MonotonicNonNull AudioFocusRequest audioFocusRequest; private boolean rebuildAudioFocusRequest; @@ -116,13 +111,14 @@ public final class AudioFocusManager { * Constructs an AudioFocusManager to automatically handle audio focus for a player. * * @param context The current context. + * @param eventHandler A {@link Handler} to for the thread on which the player is used. * @param playerControl A {@link PlayerControl} to handle commands from this instance. */ - public AudioFocusManager(Context context, PlayerControl playerControl) { + public AudioFocusManager(Context context, Handler eventHandler, PlayerControl playerControl) { this.audioManager = (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE); this.playerControl = playerControl; - this.focusListener = new AudioFocusListener(); + this.focusListener = new AudioFocusListener(eventHandler); this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; } @@ -134,64 +130,45 @@ public final class AudioFocusManager { /** * Sets audio attributes that should be used to manage audio focus. * + *

Call {@link #updateAudioFocus(boolean, int)} to update the audio focus based on these + * attributes. + * * @param audioAttributes The audio attributes or {@code null} if audio focus should not be * managed automatically. - * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. - * @param playerState The current player state; {@link ExoPlayer#getPlaybackState()}. - * @return A {@link PlayerCommand} to execute on the player. */ - @PlayerCommand - public int setAudioAttributes( - @Nullable AudioAttributes audioAttributes, boolean playWhenReady, int playerState) { + public void setAudioAttributes(@Nullable AudioAttributes audioAttributes) { if (!Util.areEqual(this.audioAttributes, audioAttributes)) { this.audioAttributes = audioAttributes; focusGain = convertAudioAttributesToFocusGain(audioAttributes); - Assertions.checkArgument( focusGain == C.AUDIOFOCUS_GAIN || focusGain == C.AUDIOFOCUS_NONE, "Automatic handling of audio focus is only available for USAGE_MEDIA and USAGE_GAME."); - if (playWhenReady - && (playerState == Player.STATE_BUFFERING || playerState == Player.STATE_READY)) { - return requestAudioFocus(); - } } - - return playerState == Player.STATE_IDLE - ? handleIdle(playWhenReady) - : handlePrepare(playWhenReady); } /** - * Called by a player as part of {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. + * Called by the player to abandon or request audio focus based on the desired player state. * - * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. + * @param playWhenReady The desired value of playWhenReady. + * @param playbackState The desired playback state. * @return A {@link PlayerCommand} to execute on the player. */ @PlayerCommand - public int handlePrepare(boolean playWhenReady) { + public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackState) { + if (shouldAbandonAudioFocus(playbackState)) { + abandonAudioFocus(); + return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; + } return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY; } /** - * Called by the player as part of {@link ExoPlayer#setPlayWhenReady(boolean)}. - * - * @param playWhenReady The desired value of playWhenReady. - * @param playerState The current state of the player. - * @return A {@link PlayerCommand} to execute on the player. + * Called when the manager is no longer required. Audio focus will be released without making any + * calls to the {@link PlayerControl}. */ - @PlayerCommand - public int handleSetPlayWhenReady(boolean playWhenReady, int playerState) { - if (!playWhenReady) { - abandonAudioFocus(); - return PLAYER_COMMAND_DO_NOT_PLAY; - } - - return playerState == Player.STATE_IDLE ? handleIdle(playWhenReady) : requestAudioFocus(); - } - - /** Called by the player as part of {@link ExoPlayer#stop(boolean)}. */ - public void handleStop() { - abandonAudioFocus(/* forceAbandon= */ true); + public void release() { + playerControl = null; + abandonAudioFocus(); } // Internal methods. @@ -201,62 +178,35 @@ public final class AudioFocusManager { return focusListener; } - @PlayerCommand - private int handleIdle(boolean playWhenReady) { - return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; + private boolean shouldAbandonAudioFocus(@Player.State int playbackState) { + return playbackState == Player.STATE_IDLE || focusGain != C.AUDIOFOCUS_GAIN; } @PlayerCommand private int requestAudioFocus() { - int focusRequestResult; - - if (focusGain == C.AUDIOFOCUS_NONE) { - if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) { - abandonAudioFocus(/* forceAbandon= */ true); - } + if (audioFocusState == AUDIO_FOCUS_STATE_HAVE_FOCUS) { return PLAYER_COMMAND_PLAY_WHEN_READY; } - - if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { - if (Util.SDK_INT >= 26) { - focusRequestResult = requestAudioFocusV26(); - } else { - focusRequestResult = requestAudioFocusDefault(); - } - audioFocusState = - focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED - ? AUDIO_FOCUS_STATE_HAVE_FOCUS - : AUDIO_FOCUS_STATE_NO_FOCUS; - } - - if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault(); + if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); + return PLAYER_COMMAND_PLAY_WHEN_READY; + } else { + setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); return PLAYER_COMMAND_DO_NOT_PLAY; } - - return audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT - ? PLAYER_COMMAND_WAIT_FOR_CALLBACK - : PLAYER_COMMAND_PLAY_WHEN_READY; } private void abandonAudioFocus() { - abandonAudioFocus(/* forceAbandon= */ false); - } - - private void abandonAudioFocus(boolean forceAbandon) { - if (focusGain == C.AUDIOFOCUS_NONE && audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { return; } - - if (focusGain != C.AUDIOFOCUS_GAIN - || audioFocusState == AUDIO_FOCUS_STATE_LOST_FOCUS - || forceAbandon) { - if (Util.SDK_INT >= 26) { - abandonAudioFocusV26(); - } else { - abandonAudioFocusDefault(); - } - audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; + if (Util.SDK_INT >= 26) { + abandonAudioFocusV26(); + } else { + abandonAudioFocusDefault(); } + setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); } private int requestAudioFocusDefault() { @@ -310,8 +260,8 @@ public final class AudioFocusManager { * @param audioAttributes The audio attributes associated with this focus request. * @return The type of audio focus gain that should be requested. */ + @C.AudioFocusGain private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) { - if (audioAttributes == null) { // Don't handle audio focus. It may be either video only contents or developers // want to have more finer grained control. (e.g. adding audio focus listener) @@ -381,65 +331,67 @@ public final class AudioFocusManager { } } + private void setAudioFocusState(@AudioFocusState int audioFocusState) { + if (this.audioFocusState == audioFocusState) { + return; + } + this.audioFocusState = audioFocusState; + + float volumeMultiplier = + (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) + ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK + : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; + if (this.volumeMultiplier == volumeMultiplier) { + return; + } + this.volumeMultiplier = volumeMultiplier; + if (playerControl != null) { + playerControl.setVolumeMultiplier(volumeMultiplier); + } + } + + private void handlePlatformAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); + executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); + return; + case AudioManager.AUDIOFOCUS_LOSS: + executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); + abandonAudioFocus(); + return; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || willPauseWhenDucked()) { + executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); + setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT); + } else { + setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK); + } + return; + default: + Log.w(TAG, "Unknown focus change type: " + focusChange); + } + } + + private void executePlayerCommand(@PlayerCommand int playerCommand) { + if (playerControl != null) { + playerControl.executePlayerCommand(playerCommand); + } + } + // Internal audio focus listener. private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { + private final Handler eventHandler; + + public AudioFocusListener(Handler eventHandler) { + this.eventHandler = eventHandler; + } + @Override public void onAudioFocusChange(int focusChange) { - // Convert the platform focus change to internal state. - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - if (willPauseWhenDucked()) { - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; - } else { - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK; - } - break; - case AudioManager.AUDIOFOCUS_GAIN: - audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS; - break; - default: - Log.w(TAG, "Unknown focus change type: " + focusChange); - // Early return. - return; - } - - // Handle the internal state (change). - switch (audioFocusState) { - case AUDIO_FOCUS_STATE_NO_FOCUS: - // Focus was not requested; nothing to do. - break; - case AUDIO_FOCUS_STATE_LOST_FOCUS: - playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); - abandonAudioFocus(/* forceAbandon= */ true); - break; - case AUDIO_FOCUS_STATE_LOSS_TRANSIENT: - playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); - break; - case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK: - // Volume will be adjusted by the code below. - break; - case AUDIO_FOCUS_STATE_HAVE_FOCUS: - playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); - break; - default: - throw new IllegalStateException("Unknown audio focus state: " + audioFocusState); - } - - float volumeMultiplier = - (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) - ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK - : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; - if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) { - AudioFocusManager.this.volumeMultiplier = volumeMultiplier; - playerControl.setVolumeMultiplier(volumeMultiplier); - } + eventHandler.post(() -> handlePlatformAudioFocusChange(focusChange)); } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 7ccfb5709..2646cbc03 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -101,11 +101,15 @@ public abstract class BasePlayer implements Player { @Override @Nullable public final Object getCurrentTag() { - int windowIndex = getCurrentWindowIndex(); Timeline timeline = getCurrentTimeline(); - return windowIndex >= timeline.getWindowCount() - ? null - : timeline.getWindow(windowIndex, window, /* setTag= */ true).tag; + return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).tag; + } + + @Override + @Nullable + public final Object getCurrentManifest() { + Timeline timeline = getCurrentTimeline(); + return timeline.isEmpty() ? null : timeline.getWindow(getCurrentWindowIndex(), window).manifest; } @Override @@ -123,6 +127,12 @@ public abstract class BasePlayer implements Player { return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic; } + @Override + public final boolean isCurrentWindowLive() { + Timeline timeline = getCurrentTimeline(); + return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isLive; + } + @Override public final boolean isCurrentWindowSeekable() { Timeline timeline = getCurrentTimeline(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 1099b14bf..10573af41 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2; +import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -30,6 +34,7 @@ import java.io.IOException; public abstract class BaseRenderer implements Renderer, RendererCapabilities { private final int trackType; + private final FormatHolder formatHolder; private RendererConfiguration configuration; private int index; @@ -39,6 +44,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { private long streamOffsetUs; private long readingPositionUs; private boolean streamIsFinal; + private boolean throwRendererExceptionIsExecuting; /** * @param trackType The track type that the renderer handles. One of the {@link C} @@ -46,6 +52,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { */ public BaseRenderer(int trackType) { this.trackType = trackType; + formatHolder = new FormatHolder(); readingPositionUs = C.TIME_END_OF_SOURCE; } @@ -65,6 +72,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override + @Nullable public MediaClock getMediaClock() { return null; } @@ -105,6 +113,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { } @Override + @Nullable public final SampleStream getStream() { return stream; } @@ -151,6 +160,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Override public final void disable() { Assertions.checkState(state == STATE_ENABLED); + formatHolder.clear(); state = STATE_DISABLED; stream = null; streamFormats = null; @@ -161,12 +171,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Override public final void reset() { Assertions.checkState(state == STATE_DISABLED); + formatHolder.clear(); onReset(); } // RendererCapabilities implementation. @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } @@ -269,6 +281,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { // Methods to be called by subclasses. + /** Returns a clear {@link FormatHolder}. */ + protected final FormatHolder getFormatHolder() { + formatHolder.clear(); + return formatHolder; + } + /** Returns the formats of the currently enabled stream. */ protected final Format[] getStreamFormats() { return streamFormats; @@ -281,6 +299,35 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return configuration; } + /** Returns a {@link DrmSession} ready for assignment, handling resource management. */ + @Nullable + protected final DrmSession getUpdatedSourceDrmSession( + @Nullable Format oldFormat, + Format newFormat, + @Nullable DrmSessionManager drmSessionManager, + @Nullable DrmSession existingSourceSession) + throws ExoPlaybackException { + boolean drmInitDataChanged = + !Util.areEqual(newFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); + if (!drmInitDataChanged) { + return existingSourceSession; + } + @Nullable DrmSession newSourceDrmSession = null; + if (newFormat.drmInitData != null) { + if (drmSessionManager == null) { + throw createRendererException( + new IllegalStateException("Media requires a DrmSessionManager"), newFormat); + } + newSourceDrmSession = + drmSessionManager.acquireSession( + Assertions.checkNotNull(Looper.myLooper()), newFormat.drmInitData); + } + if (existingSourceSession != null) { + existingSourceSession.release(); + } + return newSourceDrmSession; + } + /** * Returns the index of the renderer within the player. */ @@ -288,6 +335,30 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return index; } + /** + * Creates an {@link ExoPlaybackException} of type {@link ExoPlaybackException#TYPE_RENDERER} for + * this renderer. + * + * @param cause The cause of the exception. + * @param format The current format used by the renderer. May be null. + */ + protected final ExoPlaybackException createRendererException( + Exception cause, @Nullable Format format) { + @FormatSupport int formatSupport = RendererCapabilities.FORMAT_HANDLED; + if (format != null && !throwRendererExceptionIsExecuting) { + // Prevent recursive re-entry from subclass supportsFormat implementations. + throwRendererExceptionIsExecuting = true; + try { + formatSupport = RendererCapabilities.getFormatSupport(supportsFormat(format)); + } catch (ExoPlaybackException e) { + // Ignore, we are already failing. + } finally { + throwRendererExceptionIsExecuting = false; + } + } + return ExoPlaybackException.createForRenderer(cause, getIndex(), format, formatSupport); + } + /** * Reads from the enabled upstream source. If the upstream source has been read to the end then * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been @@ -295,16 +366,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If the end of the stream has been reached, the - * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. + * end of the stream. If the end of the stream has been reached, the {@link + * C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. * @param formatRequired Whether the caller requires that the format of the stream be read even if * it's not changing. A sample will never be read if set to true, however it is still possible * for the end of stream or nothing to be read. * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ - protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer, - boolean formatRequired) { + protected final int readSource( + FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { int result = stream.readData(formatHolder, buffer, formatRequired); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/C.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/C.java index 56f949485..3eee0a189 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/C.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/C.java @@ -17,15 +17,18 @@ package com.google.android.exoplayer2; import android.annotation.TargetApi; import android.content.Context; +import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; import android.media.MediaCodec; import android.media.MediaFormat; -import androidx.annotation.IntDef; import android.view.Surface; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.SimpleDecoderVideoRenderer; +import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.spherical.CameraMotionListener; import java.lang.annotation.Documented; @@ -92,14 +95,16 @@ public final class C { * The name of the ASCII charset. */ public static final String ASCII_NAME = "US-ASCII"; + /** * The name of the UTF-8 charset. */ public static final String UTF8_NAME = "UTF-8"; - /** - * The name of the UTF-16 charset. - */ + /** The name of the ISO-8859-1 charset. */ + public static final String ISO88591_NAME = "ISO-8859-1"; + + /** The name of the UTF-16 charset. */ public static final String UTF16_NAME = "UTF-16"; /** The name of the UTF-16 little-endian charset. */ @@ -145,8 +150,8 @@ public final class C { /** * Represents an audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link - * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link - * #ENCODING_PCM_MU_LAW}, {@link #ENCODING_PCM_A_LAW}, {@link #ENCODING_AC3}, {@link + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link * #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS}, * {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}. */ @@ -157,26 +162,26 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW, + ENCODING_MP3, ENCODING_AC3, ENCODING_E_AC3, ENCODING_E_AC3_JOC, ENCODING_AC4, ENCODING_DTS, ENCODING_DTS_HD, - ENCODING_DOLBY_TRUEHD, + ENCODING_DOLBY_TRUEHD }) public @interface Encoding {} /** * Represents a PCM audio encoding, or an invalid or unset value. One of {@link Format#NO_VALUE}, * {@link #ENCODING_INVALID}, {@link #ENCODING_PCM_8BIT}, {@link #ENCODING_PCM_16BIT}, {@link - * #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, {@link #ENCODING_PCM_FLOAT}, {@link - * #ENCODING_PCM_MU_LAW} or {@link #ENCODING_PCM_A_LAW}. + * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, + * {@link #ENCODING_PCM_FLOAT}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -185,11 +190,10 @@ public final class C { ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, + ENCODING_PCM_16BIT_BIG_ENDIAN, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, - ENCODING_PCM_FLOAT, - ENCODING_PCM_MU_LAW, - ENCODING_PCM_A_LAW + ENCODING_PCM_FLOAT }) public @interface PcmEncoding {} /** @see AudioFormat#ENCODING_INVALID */ @@ -198,16 +202,16 @@ public final class C { 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; + /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ + public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000; /** PCM encoding with 24 bits per sample. */ - public static final int ENCODING_PCM_24BIT = 0x80000000; + public static final int ENCODING_PCM_24BIT = 0x20000000; /** PCM encoding with 32 bits per sample. */ - public static final int ENCODING_PCM_32BIT = 0x40000000; + public static final int ENCODING_PCM_32BIT = 0x30000000; /** @see AudioFormat#ENCODING_PCM_FLOAT */ public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; - /** Audio encoding for mu-law. */ - public static final int ENCODING_PCM_MU_LAW = 0x10000000; - /** Audio encoding for A-law. */ - public static final int ENCODING_PCM_A_LAW = 0x20000000; + /** @see AudioFormat#ENCODING_MP3 */ + public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; /** @see AudioFormat#ENCODING_AC3 */ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; /** @see AudioFormat#ENCODING_E_AC3 */ @@ -440,6 +444,21 @@ public final class C { public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; + /** + * Capture policies for {@link com.google.android.exoplayer2.audio.AudioAttributes}. One of {@link + * #ALLOW_CAPTURE_BY_ALL}, {@link #ALLOW_CAPTURE_BY_NONE} or {@link #ALLOW_CAPTURE_BY_SYSTEM}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ALLOW_CAPTURE_BY_ALL, ALLOW_CAPTURE_BY_NONE, ALLOW_CAPTURE_BY_SYSTEM}) + public @interface AudioAllowedCapturePolicy {} + /** See {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_ALL}. */ + public static final int ALLOW_CAPTURE_BY_ALL = AudioAttributes.ALLOW_CAPTURE_BY_ALL; + /** See {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_NONE}. */ + public static final int ALLOW_CAPTURE_BY_NONE = AudioAttributes.ALLOW_CAPTURE_BY_NONE; + /** See {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM}. */ + public static final int ALLOW_CAPTURE_BY_SYSTEM = AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM; + /** * Audio focus types. One of {@link #AUDIOFOCUS_NONE}, {@link #AUDIOFOCUS_GAIN}, {@link * #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or {@link @@ -480,6 +499,7 @@ public final class C { value = { BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM, + BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA, BUFFER_FLAG_LAST_SAMPLE, BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY @@ -493,14 +513,35 @@ public final class C { * Flag for empty buffers that signal that the end of the stream was reached. */ public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; + /** Indicates that a buffer has supplemental data. */ + public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000 /** Indicates that a buffer is known to contain the last media sample of the stream. */ public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000 /** Indicates that a buffer is (at least partially) encrypted. */ public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 /** Indicates that a buffer should be decoded but not rendered. */ - @SuppressWarnings("NumericOverflow") public static final int BUFFER_FLAG_DECODE_ONLY = 1 << 31; // 0x80000000 + // LINT.IfChange + /** + * Video decoder output modes. Possible modes are {@link #VIDEO_OUTPUT_MODE_NONE}, {@link + * #VIDEO_OUTPUT_MODE_YUV} and {@link #VIDEO_OUTPUT_MODE_SURFACE_YUV}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {VIDEO_OUTPUT_MODE_NONE, VIDEO_OUTPUT_MODE_YUV, VIDEO_OUTPUT_MODE_SURFACE_YUV}) + public @interface VideoOutputMode {} + /** Video decoder output mode is not set. */ + public static final int VIDEO_OUTPUT_MODE_NONE = -1; + /** Video decoder output mode that outputs raw 4:2:0 YUV planes. */ + public static final int VIDEO_OUTPUT_MODE_YUV = 0; + /** Video decoder output mode that renders 4:2:0 YUV planes directly to a surface. */ + public static final int VIDEO_OUTPUT_MODE_SURFACE_YUV = 1; + // LINT.ThenChange( + // ../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc, + // ../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc + // ) + /** * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s. One of {@link * #VIDEO_SCALING_MODE_SCALE_TO_FIT} or {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}. @@ -790,6 +831,17 @@ public final class C { */ public static final int MSG_SET_CAMERA_MOTION_LISTENER = 7; + /** + * The type of a message that can be passed to a {@link SimpleDecoderVideoRenderer} via {@link + * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link + * VideoDecoderOutputBufferRenderer}, or null. + * + *

This message is intended only for use with extension renderers that expect a {@link + * VideoDecoderOutputBufferRenderer}. For other use cases, an output surface should be passed via + * {@link #MSG_SET_SURFACE} instead. + */ + public static final int MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER = 8; + /** * Applications or extensions may define custom {@code MSG_*} constants that can be passed to * {@link Renderer}s. These custom constants must be greater than or equal to this value. @@ -925,8 +977,8 @@ public final class C { /** * Network connection type. One of {@link #NETWORK_TYPE_UNKNOWN}, {@link #NETWORK_TYPE_OFFLINE}, * {@link #NETWORK_TYPE_WIFI}, {@link #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, {@link - * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link #NETWORK_TYPE_ETHERNET} or - * {@link #NETWORK_TYPE_OTHER}. + * #NETWORK_TYPE_4G}, {@link #NETWORK_TYPE_5G}, {@link #NETWORK_TYPE_CELLULAR_UNKNOWN}, {@link + * #NETWORK_TYPE_ETHERNET} or {@link #NETWORK_TYPE_OTHER}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -937,6 +989,7 @@ public final class C { NETWORK_TYPE_2G, NETWORK_TYPE_3G, NETWORK_TYPE_4G, + NETWORK_TYPE_5G, NETWORK_TYPE_CELLULAR_UNKNOWN, NETWORK_TYPE_ETHERNET, NETWORK_TYPE_OTHER @@ -954,6 +1007,8 @@ public final class C { public static final int NETWORK_TYPE_3G = 4; /** Network type for a 4G cellular connection. */ public static final int NETWORK_TYPE_4G = 5; + /** Network type for a 5G cellular connection. */ + public static final int NETWORK_TYPE_5G = 9; /** * Network type for cellular connections which cannot be mapped to one of {@link * #NETWORK_TYPE_2G}, {@link #NETWORK_TYPE_3G}, or {@link #NETWORK_TYPE_4G}. @@ -961,19 +1016,48 @@ public final class C { public static final int NETWORK_TYPE_CELLULAR_UNKNOWN = 6; /** Network type for an Ethernet connection. */ public static final int NETWORK_TYPE_ETHERNET = 7; - /** - * Network type for other connections which are not Wifi or cellular (e.g. Ethernet, VPN, - * Bluetooth). - */ + /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */ public static final int NETWORK_TYPE_OTHER = 8; + /** + * Mode specifying whether the player should hold a WakeLock and a WifiLock. One of {@link + * #WAKE_MODE_NONE}, {@link #WAKE_MODE_LOCAL} and {@link #WAKE_MODE_NETWORK}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({WAKE_MODE_NONE, WAKE_MODE_LOCAL, WAKE_MODE_NETWORK}) + public @interface WakeMode {} + /** + * A wake mode that will not cause the player to hold any locks. + * + *

This is suitable for applications that do not play media with the screen off. + */ + public static final int WAKE_MODE_NONE = 0; + /** + * A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} + * during playback. + * + *

This is suitable for applications that play media with the screen off and do not load media + * over wifi. + */ + public static final int WAKE_MODE_LOCAL = 1; + /** + * A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} and a + * {@link android.net.wifi.WifiManager.WifiLock} during playback. + * + *

This is suitable for applications that play media with the screen off and may load media + * over wifi. + */ + public static final int WAKE_MODE_NETWORK = 2; + /** * Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link * #ROLE_FLAG_SUBTITLE}, {@link #ROLE_FLAG_SIGN}, {@link #ROLE_FLAG_DESCRIBES_VIDEO}, {@link * #ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND}, {@link #ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY}, - * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG} and {@link #ROLE_FLAG_EASY_TO_READ}. + * {@link #ROLE_FLAG_TRANSCRIBES_DIALOG}, {@link #ROLE_FLAG_EASY_TO_READ} and {@link + * #ROLE_FLAG_TRICK_PLAY}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -993,7 +1077,8 @@ public final class C { ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND, ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY, ROLE_FLAG_TRANSCRIBES_DIALOG, - ROLE_FLAG_EASY_TO_READ + ROLE_FLAG_EASY_TO_READ, + ROLE_FLAG_TRICK_PLAY }) public @interface RoleFlags {} /** Indicates a main track. */ @@ -1039,6 +1124,8 @@ public final class C { public static final int ROLE_FLAG_TRANSCRIBES_DIALOG = 1 << 12; /** Indicates the track contains a text that has been edited for ease of reading. */ public static final int ROLE_FLAG_EASY_TO_READ = 1 << 13; + /** Indicates the track is intended for trick play. */ + public static final int ROLE_FLAG_TRICK_PLAY = 1 << 14; /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java index 89e7d857c..1971a4cef 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -32,19 +32,21 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; public interface PlaybackParameterListener { /** - * Called when the active playback parameters changed. + * Called when the active playback parameters changed. Will not be called for {@link + * #setPlaybackParameters(PlaybackParameters)}. * * @param newPlaybackParameters The newly active {@link PlaybackParameters}. */ void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); - } - private final StandaloneMediaClock standaloneMediaClock; + private final StandaloneMediaClock standaloneClock; private final PlaybackParameterListener listener; - private @Nullable Renderer rendererClockSource; - private @Nullable MediaClock rendererClock; + @Nullable private Renderer rendererClockSource; + @Nullable private MediaClock rendererClock; + private boolean isUsingStandaloneClock; + private boolean standaloneClockIsStarted; /** * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use @@ -56,21 +58,24 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; */ public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) { this.listener = listener; - this.standaloneMediaClock = new StandaloneMediaClock(clock); + this.standaloneClock = new StandaloneMediaClock(clock); + isUsingStandaloneClock = true; } /** * Starts the standalone fallback clock. */ public void start() { - standaloneMediaClock.start(); + standaloneClockIsStarted = true; + standaloneClock.start(); } /** * Stops the standalone fallback clock. */ public void stop() { - standaloneMediaClock.stop(); + standaloneClockIsStarted = false; + standaloneClock.stop(); } /** @@ -79,7 +84,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; * @param positionUs The position to set in microseconds. */ public void resetPosition(long positionUs) { - standaloneMediaClock.resetPosition(positionUs); + standaloneClock.resetPosition(positionUs); } /** @@ -99,8 +104,7 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; } this.rendererClock = rendererMediaClock; this.rendererClockSource = renderer; - rendererClock.setPlaybackParameters(standaloneMediaClock.getPlaybackParameters()); - ensureSynced(); + rendererClock.setPlaybackParameters(standaloneClock.getPlaybackParameters()); } } @@ -114,65 +118,80 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; if (renderer == rendererClockSource) { this.rendererClock = null; this.rendererClockSource = null; + isUsingStandaloneClock = true; } } /** * Syncs internal clock if needed and returns current clock position in microseconds. + * + * @param isReadingAhead Whether the renderers are reading ahead. */ - public long syncAndGetPositionUs() { - if (isUsingRendererClock()) { - ensureSynced(); - return rendererClock.getPositionUs(); - } else { - return standaloneMediaClock.getPositionUs(); - } + public long syncAndGetPositionUs(boolean isReadingAhead) { + syncClocks(isReadingAhead); + return getPositionUs(); } // MediaClock implementation. @Override public long getPositionUs() { - if (isUsingRendererClock()) { - return rendererClock.getPositionUs(); - } else { - return standaloneMediaClock.getPositionUs(); - } + return isUsingStandaloneClock ? standaloneClock.getPositionUs() : rendererClock.getPositionUs(); } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (rendererClock != null) { - playbackParameters = rendererClock.setPlaybackParameters(playbackParameters); + rendererClock.setPlaybackParameters(playbackParameters); + playbackParameters = rendererClock.getPlaybackParameters(); } - standaloneMediaClock.setPlaybackParameters(playbackParameters); - listener.onPlaybackParametersChanged(playbackParameters); - return playbackParameters; + standaloneClock.setPlaybackParameters(playbackParameters); } @Override public PlaybackParameters getPlaybackParameters() { - return rendererClock != null ? rendererClock.getPlaybackParameters() - : standaloneMediaClock.getPlaybackParameters(); + return rendererClock != null + ? rendererClock.getPlaybackParameters() + : standaloneClock.getPlaybackParameters(); } - private void ensureSynced() { + private void syncClocks(boolean isReadingAhead) { + if (shouldUseStandaloneClock(isReadingAhead)) { + isUsingStandaloneClock = true; + if (standaloneClockIsStarted) { + standaloneClock.start(); + } + return; + } long rendererClockPositionUs = rendererClock.getPositionUs(); - standaloneMediaClock.resetPosition(rendererClockPositionUs); + if (isUsingStandaloneClock) { + // Ensure enabling the renderer clock doesn't jump backwards in time. + if (rendererClockPositionUs < standaloneClock.getPositionUs()) { + standaloneClock.stop(); + return; + } + isUsingStandaloneClock = false; + if (standaloneClockIsStarted) { + standaloneClock.start(); + } + } + // Continuously sync stand-alone clock to renderer clock so that it can take over if needed. + standaloneClock.resetPosition(rendererClockPositionUs); PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters(); - if (!playbackParameters.equals(standaloneMediaClock.getPlaybackParameters())) { - standaloneMediaClock.setPlaybackParameters(playbackParameters); + if (!playbackParameters.equals(standaloneClock.getPlaybackParameters())) { + standaloneClock.setPlaybackParameters(playbackParameters); listener.onPlaybackParametersChanged(playbackParameters); } } - private boolean isUsingRendererClock() { - // Use the renderer clock if the providing renderer has not ended or needs the next sample - // stream to reenter the ready state. The latter case uses the standalone clock to avoid getting - // stuck if tracks in the current period have uneven durations. - // See: https://github.com/google/ExoPlayer/issues/1874. - return rendererClockSource != null && !rendererClockSource.isEnded() - && (rendererClockSource.isReady() || !rendererClockSource.hasReadStreamToEnd()); + private boolean shouldUseStandaloneClock(boolean isReadingAhead) { + // Use the standalone clock if the clock providing renderer is not set or has ended. Also use + // the standalone clock if the renderer is not ready and we have finished reading the stream or + // are reading ahead to avoid getting stuck if tracks in the current period have uneven + // durations. See: https://github.com/google/ExoPlayer/issues/1874. + return rendererClockSource == null + || rendererClockSource.isEnded() + || (!rendererClockSource.isReady() + && (isReadingAhead || rendererClockSource.hasReadStreamToEnd())); } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 490d96139..f53d72f59 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -104,7 +104,7 @@ public class DefaultRenderersFactory implements RenderersFactory { /** * @deprecated Use {@link #DefaultRenderersFactory(Context)} and pass {@link DrmSessionManager} - * directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + * directly to {@link SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -127,7 +127,7 @@ public class DefaultRenderersFactory implements RenderersFactory { /** * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link - * SimpleExoPlayer} or {@link ExoPlayerFactory}. + * SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -154,7 +154,7 @@ public class DefaultRenderersFactory implements RenderersFactory { /** * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass - * {@link DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + * {@link DrmSessionManager} directly to {@link SimpleExoPlayer.Builder}. */ @Deprecated public DefaultRenderersFactory( @@ -365,6 +365,33 @@ public class DefaultRenderersFactory implements RenderersFactory { // The extension is present, but instantiation failed. throw new RuntimeException("Error instantiating VP9 extension", e); } + + try { + // Full class names used for constructor args so the LINT rule triggers if any of them move. + // LINT.IfChange + Class clazz = Class.forName("com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer"); + Constructor constructor = + clazz.getConstructor( + long.class, + android.os.Handler.class, + com.google.android.exoplayer2.video.VideoRendererEventListener.class, + int.class); + // LINT.ThenChange(../../../../../../../proguard-rules.txt) + Renderer renderer = + (Renderer) + constructor.newInstance( + allowedVideoJoiningTimeMs, + eventHandler, + eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); + out.add(extensionRendererIndex++, renderer); + Log.i(TAG, "Loaded Libgav1VideoRenderer."); + } catch (ClassNotFoundException e) { + // Expected if the app was built without the extension. + } catch (Exception e) { + // The extension is present, but instantiation failed. + throw new RuntimeException("Error instantiating AV1 extension", e); + } } /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index b5f8f954b..653b6002d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2; +import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -73,6 +75,22 @@ public final class ExoPlaybackException extends Exception { */ public final int rendererIndex; + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the {@link Format} the renderer was using + * at the time of the exception, or null if the renderer wasn't using a {@link Format}. + */ + @Nullable public final Format rendererFormat; + + /** + * If {@link #type} is {@link #TYPE_RENDERER}, this is the level of {@link FormatSupport} of the + * renderer for {@link #rendererFormat}. If {@link #rendererFormat} is null, this is {@link + * RendererCapabilities#FORMAT_HANDLED}. + */ + @FormatSupport public final int rendererFormatSupport; + + /** The value of {@link SystemClock#elapsedRealtime()} when this exception was created. */ + public final long timestampMs; + @Nullable private final Throwable cause; /** @@ -82,7 +100,7 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForSource(IOException cause) { - return new ExoPlaybackException(TYPE_SOURCE, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_SOURCE, cause); } /** @@ -90,10 +108,23 @@ public final class ExoPlaybackException extends Exception { * * @param cause The cause of the failure. * @param rendererIndex The index of the renderer in which the failure occurred. + * @param rendererFormat The {@link Format} the renderer was using at the time of the exception, + * or null if the renderer wasn't using a {@link Format}. + * @param rendererFormatSupport The {@link FormatSupport} of the renderer for {@code + * rendererFormat}. Ignored if {@code rendererFormat} is null. * @return The created instance. */ - public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) { - return new ExoPlaybackException(TYPE_RENDERER, cause, rendererIndex); + public static ExoPlaybackException createForRenderer( + Exception cause, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { + return new ExoPlaybackException( + TYPE_RENDERER, + cause, + rendererIndex, + rendererFormat, + rendererFormat == null ? RendererCapabilities.FORMAT_HANDLED : rendererFormatSupport); } /** @@ -103,7 +134,7 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForUnexpected(RuntimeException cause) { - return new ExoPlaybackException(TYPE_UNEXPECTED, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_UNEXPECTED, cause); } /** @@ -123,21 +154,41 @@ public final class ExoPlaybackException extends Exception { * @return The created instance. */ public static ExoPlaybackException createForOutOfMemoryError(OutOfMemoryError cause) { - return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause, /* rendererIndex= */ C.INDEX_UNSET); + return new ExoPlaybackException(TYPE_OUT_OF_MEMORY, cause); } - private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) { + private ExoPlaybackException(@Type int type, Throwable cause) { + this( + type, + cause, + /* rendererIndex= */ C.INDEX_UNSET, + /* rendererFormat= */ null, + /* rendererFormatSupport= */ RendererCapabilities.FORMAT_HANDLED); + } + + private ExoPlaybackException( + @Type int type, + Throwable cause, + int rendererIndex, + @Nullable Format rendererFormat, + @FormatSupport int rendererFormatSupport) { super(cause); this.type = type; this.cause = cause; this.rendererIndex = rendererIndex; + this.rendererFormat = rendererFormat; + this.rendererFormatSupport = rendererFormatSupport; + timestampMs = SystemClock.elapsedRealtime(); } private ExoPlaybackException(@Type int type, String message) { super(message); this.type = type; rendererIndex = C.INDEX_UNSET; + rendererFormat = null; + rendererFormatSupport = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; cause = null; + timestampMs = SystemClock.elapsedRealtime(); } /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index d0f9e2ae0..c2e5c7170 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -15,8 +15,11 @@ */ package com.google.android.exoplayer2; +import android.content.Context; import android.os.Looper; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ClippingMediaSource; @@ -29,12 +32,17 @@ import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link - * ExoPlayerFactory}. + * SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder}. * *

Player components

* @@ -85,8 +93,8 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * *

The figure below shows ExoPlayer's threading model. * - *

ExoPlayer's threading
- * model + *

ExoPlayer's
+ * threading model * *

    *
  • ExoPlayer instances must be accessed from a single application thread. For the vast @@ -117,27 +125,200 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; */ public interface ExoPlayer extends Player { - /** @deprecated Use {@link PlayerMessage.Target} instead. */ - @Deprecated - interface ExoPlayerComponent extends PlayerMessage.Target {} + /** + * A builder for {@link ExoPlayer} instances. + * + *

    See {@link #Builder(Context, Renderer...)} for the list of default values. + */ + final class Builder { - /** @deprecated Use {@link PlayerMessage} instead. */ - @Deprecated - final class ExoPlayerMessage { + private final Renderer[] renderers; - /** The target to receive the message. */ - public final PlayerMessage.Target target; - /** The type of the message. */ - public final int messageType; - /** The message. */ - public final Object message; + private Clock clock; + private TrackSelector trackSelector; + private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; + private Looper looper; + private AnalyticsCollector analyticsCollector; + private boolean useLazyPreparation; + private boolean buildCalled; - /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ - @Deprecated - public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { - this.target = target; - this.messageType = messageType; - this.message = message; + /** + * Creates a builder with a list of {@link Renderer Renderers}. + * + *

    The builder uses the following default values: + * + *

      + *
    • {@link TrackSelector}: {@link DefaultTrackSelector} + *
    • {@link LoadControl}: {@link DefaultLoadControl} + *
    • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} + *
    • {@link Looper}: The {@link Looper} associated with the current thread, or the {@link + * Looper} of the application's main thread if the current thread doesn't have a {@link + * Looper} + *
    • {@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT} + *
    • {@code useLazyPreparation}: {@code true} + *
    • {@link Clock}: {@link Clock#DEFAULT} + *
    + * + * @param context A {@link Context}. + * @param renderers The {@link Renderer Renderers} to be used by the player. + */ + public Builder(Context context, Renderer... renderers) { + this( + renderers, + new DefaultTrackSelector(context), + new DefaultLoadControl(), + DefaultBandwidthMeter.getSingletonInstance(context), + Util.getLooper(), + new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, + Clock.DEFAULT); + } + + /** + * Creates a builder with the specified custom components. + * + *

    Note that this constructor is only useful if you try to ensure that ExoPlayer's default + * components can be removed by ProGuard or R8. For most components except renderers, there is + * only a marginal benefit of doing that. + * + * @param renderers The {@link Renderer Renderers} to be used by the player. + * @param trackSelector A {@link TrackSelector}. + * @param loadControl A {@link LoadControl}. + * @param bandwidthMeter A {@link BandwidthMeter}. + * @param looper A {@link Looper} that must be used for all calls to the player. + * @param analyticsCollector An {@link AnalyticsCollector}. + * @param useLazyPreparation Whether media sources should be initialized lazily. + * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. + */ + public Builder( + Renderer[] renderers, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Looper looper, + AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, + Clock clock) { + Assertions.checkArgument(renderers.length > 0); + this.renderers = renderers; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; + this.looper = looper; + this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; + this.clock = clock; + } + + /** + * Sets the {@link TrackSelector} that will be used by the player. + * + * @param trackSelector A {@link TrackSelector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setTrackSelector(TrackSelector trackSelector) { + Assertions.checkState(!buildCalled); + this.trackSelector = trackSelector; + return this; + } + + /** + * Sets the {@link LoadControl} that will be used by the player. + * + * @param loadControl A {@link LoadControl}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLoadControl(LoadControl loadControl) { + Assertions.checkState(!buildCalled); + this.loadControl = loadControl; + return this; + } + + /** + * Sets the {@link BandwidthMeter} that will be used by the player. + * + * @param bandwidthMeter A {@link BandwidthMeter}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { + Assertions.checkState(!buildCalled); + this.bandwidthMeter = bandwidthMeter; + return this; + } + + /** + * Sets the {@link Looper} that must be used for all calls to the player and that is used to + * call listeners on. + * + * @param looper A {@link Looper}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLooper(Looper looper) { + Assertions.checkState(!buildCalled); + this.looper = looper; + return this; + } + + /** + * Sets the {@link AnalyticsCollector} that will collect and forward all player events. + * + * @param analyticsCollector An {@link AnalyticsCollector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) { + Assertions.checkState(!buildCalled); + this.analyticsCollector = analyticsCollector; + return this; + } + + /** + * Sets whether media sources should be initialized lazily. + * + *

    If false, all initial preparation steps (e.g., manifest loads) happen immediately. If + * true, these initial preparations are triggered only when the player starts buffering the + * media. + * + * @param useLazyPreparation Whether to use lazy preparation. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setUseLazyPreparation(boolean useLazyPreparation) { + Assertions.checkState(!buildCalled); + this.useLazyPreparation = useLazyPreparation; + return this; + } + + /** + * Sets the {@link Clock} that will be used by the player. Should only be set for testing + * purposes. + * + * @param clock A {@link Clock}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @VisibleForTesting + public Builder setClock(Clock clock) { + Assertions.checkState(!buildCalled); + this.clock = clock; + return this; + } + + /** + * Builds an {@link ExoPlayer} instance. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public ExoPlayer build() { + Assertions.checkState(!buildCalled); + buildCalled = true; + return new ExoPlayerImpl( + renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); } } @@ -151,8 +332,8 @@ public interface ExoPlayer extends Player { void retry(); /** - * Prepares the player to play the provided {@link MediaSource}. Equivalent to - * {@code prepare(mediaSource, true, true)}. + * Prepares the player to play the provided {@link MediaSource}. Equivalent to {@code + * prepare(mediaSource, true, true)}. */ void prepare(MediaSource mediaSource); @@ -181,19 +362,6 @@ public interface ExoPlayer extends Player { */ PlayerMessage createMessage(PlayerMessage.Target target); - /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ - @Deprecated - @SuppressWarnings("deprecation") - void sendMessages(ExoPlayerMessage... messages); - - /** - * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link - * PlayerMessage#blockUntilDelivered()}. - */ - @Deprecated - @SuppressWarnings("deprecation") - void blockingSendMessages(ExoPlayerMessage... messages); - /** * Sets the parameters that control how seek operations are performed. * diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 59647feaa..e4f239df7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -28,30 +29,19 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; -/** - * A factory for {@link ExoPlayer} instances. - */ +/** @deprecated Use {@link SimpleExoPlayer.Builder} or {@link ExoPlayer.Builder} instead. */ +@Deprecated public final class ExoPlayerFactory { - private static @Nullable BandwidthMeter singletonBandwidthMeter; - private ExoPlayerFactory() {} /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. - * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector, - * LoadControl, DrmSessionManager)}. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, @@ -65,22 +55,12 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. - * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to - * seamlessly join an ongoing playback. - * @deprecated Use {@link #newSimpleInstance(Context, RenderersFactory, TrackSelector, - * LoadControl, DrmSessionManager)}. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, @@ -96,44 +76,31 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, loadControl, drmSessionManager); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance(Context context) { - return newSimpleInstance(context, new DefaultTrackSelector()); + return newSimpleInstance(context, new DefaultTrackSelector(context)); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) { return newSimpleInstance(context, new DefaultRenderersFactory(context), trackSelector); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, TrackSelector trackSelector) { return newSimpleInstance(context, renderersFactory, trackSelector, new DefaultLoadControl()); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, LoadControl loadControl) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context); @@ -141,14 +108,12 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used. - * - * @param context A {@link Context}. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, TrackSelector trackSelector, @@ -160,14 +125,12 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -177,14 +140,9 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, new DefaultLoadControl(), drmSessionManager); } - /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - */ + /** @deprecated Use {@link SimpleExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -200,15 +158,12 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -220,16 +175,12 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -244,51 +195,41 @@ public final class ExoPlayerFactory { loadControl, drmSessionManager, bandwidthMeter, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), Util.getLooper()); } /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, - AnalyticsCollector.Factory analyticsCollectorFactory) { + AnalyticsCollector analyticsCollector) { return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, drmSessionManager, - analyticsCollectorFactory, + analyticsCollector, Util.getLooper()); } /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -302,31 +243,24 @@ public final class ExoPlayerFactory { trackSelector, loadControl, drmSessionManager, - new AnalyticsCollector.Factory(), + new AnalyticsCollector(Clock.DEFAULT), looper); } /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { return newSimpleInstance( context, @@ -334,25 +268,18 @@ public final class ExoPlayerFactory { trackSelector, loadControl, drmSessionManager, - getDefaultBandwidthMeter(context), - analyticsCollectorFactory, + DefaultBandwidthMeter.getSingletonInstance(context), + analyticsCollector, looper); } /** - * Creates a {@link SimpleExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. + * @deprecated Use {@link SimpleExoPlayer.Builder} instead. The {@link DrmSessionManager} cannot + * be passed to {@link SimpleExoPlayer.Builder} and should instead be injected into the {@link + * MediaSource} factories. */ + @SuppressWarnings("deprecation") + @Deprecated public static SimpleExoPlayer newSimpleInstance( Context context, RenderersFactory renderersFactory, @@ -360,7 +287,7 @@ public final class ExoPlayerFactory { LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Looper looper) { return new SimpleExoPlayer( context, @@ -369,45 +296,30 @@ public final class ExoPlayerFactory { loadControl, drmSessionManager, bandwidthMeter, - analyticsCollectorFactory, + analyticsCollector, + Clock.DEFAULT, looper); } - /** - * Creates an {@link ExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - */ + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static ExoPlayer newInstance( Context context, Renderer[] renderers, TrackSelector trackSelector) { return newInstance(context, renderers, trackSelector, new DefaultLoadControl()); } - /** - * Creates an {@link ExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - */ + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static ExoPlayer newInstance( Context context, Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { return newInstance(context, renderers, trackSelector, loadControl, Util.getLooper()); } - /** - * Creates an {@link ExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - */ + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public static ExoPlayer newInstance( Context context, Renderer[] renderers, @@ -415,21 +327,16 @@ public final class ExoPlayerFactory { LoadControl loadControl, Looper looper) { return newInstance( - context, renderers, trackSelector, loadControl, getDefaultBandwidthMeter(context), looper); + context, + renderers, + trackSelector, + loadControl, + DefaultBandwidthMeter.getSingletonInstance(context), + looper); } - /** - * Creates an {@link ExoPlayer} instance. - * - * @param context A {@link Context}. - * @param renderers The {@link Renderer}s that will be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - */ - @SuppressWarnings("unused") + /** @deprecated Use {@link ExoPlayer.Builder} instead. */ + @Deprecated public static ExoPlayer newInstance( Context context, Renderer[] renderers, @@ -440,11 +347,4 @@ public final class ExoPlayerFactory { return new ExoPlayerImpl( renderers, trackSelector, loadControl, bandwidthMeter, Clock.DEFAULT, looper); } - - private static synchronized BandwidthMeter getDefaultBandwidthMeter(Context context) { - if (singletonBandwidthMeter == null) { - singletonBandwidthMeter = new DefaultBandwidthMeter.Builder(context).build(); - } - return singletonBandwidthMeter; - } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 79de06f99..ca46e3138 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -19,8 +19,8 @@ import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; import android.os.Message; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -35,11 +35,11 @@ import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -/** An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}. */ +/** + * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayer.Builder}. + */ /* package */ final class ExoPlayerImpl extends BasePlayer implements ExoPlayer { private static final String TAG = "ExoPlayerImpl"; @@ -71,9 +71,9 @@ import java.util.concurrent.CopyOnWriteArrayList; private boolean hasPendingPrepare; private boolean hasPendingSeek; private boolean foregroundMode; + private int pendingSetPlaybackParametersAcks; private PlaybackParameters playbackParameters; private SeekParameters seekParameters; - private @Nullable ExoPlaybackException playbackError; // Playback information when there is no pending seek/set source operation. private PlaybackInfo playbackInfo; @@ -194,10 +194,12 @@ import java.util.concurrent.CopyOnWriteArrayList; } @Override + @State public int getPlaybackState() { return playbackInfo.playbackState; } + @Override @PlaybackSuppressionReason public int getPlaybackSuppressionReason() { return playbackSuppressionReason; @@ -206,13 +208,12 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override @Nullable public ExoPlaybackException getPlaybackError() { - return playbackError; + return playbackInfo.playbackError; } @Override public void retry() { - if (mediaSource != null - && (playbackError != null || playbackInfo.playbackState == Player.STATE_IDLE)) { + if (mediaSource != null && playbackInfo.playbackState == Player.STATE_IDLE) { prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); } } @@ -224,11 +225,13 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - playbackError = null; this.mediaSource = mediaSource; PlaybackInfo playbackInfo = getResetPlaybackInfo( - resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING); + resetPosition, + resetState, + /* resetError= */ true, + /* playbackState= */ Player.STATE_BUFFERING); // Trigger internal prepare first before updating the playback info and notifying external // listeners to ensure that new operations issued in the listener notifications reach the // player after this prepare. The internal player can't change the playback info immediately @@ -244,6 +247,7 @@ import java.util.concurrent.CopyOnWriteArrayList; /* seekProcessed= */ false); } + @Override public void setPlayWhenReady(boolean playWhenReady) { setPlayWhenReady(playWhenReady, PLAYBACK_SUPPRESSION_REASON_NONE); @@ -363,7 +367,14 @@ import java.util.concurrent.CopyOnWriteArrayList; if (playbackParameters == null) { playbackParameters = PlaybackParameters.DEFAULT; } + if (this.playbackParameters.equals(playbackParameters)) { + return; + } + pendingSetPlaybackParametersAcks++; + this.playbackParameters = playbackParameters; internalPlayer.setPlaybackParameters(playbackParameters); + PlaybackParameters playbackParametersToNotify = playbackParameters; + notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParametersToNotify)); } @Override @@ -398,13 +409,13 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void stop(boolean reset) { if (reset) { - playbackError = null; mediaSource = null; } PlaybackInfo playbackInfo = getResetPlaybackInfo( /* resetPosition= */ reset, /* resetState= */ reset, + /* resetError= */ reset, /* playbackState= */ Player.STATE_IDLE); // Trigger internal stop first before updating the playback info and notifying external // listeners to ensure that new operations issued in the listener notifications reach the @@ -432,18 +443,10 @@ import java.util.concurrent.CopyOnWriteArrayList; getResetPlaybackInfo( /* resetPosition= */ false, /* resetState= */ false, + /* resetError= */ false, /* playbackState= */ Player.STATE_IDLE); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void sendMessages(ExoPlayerMessage... messages) { - for (ExoPlayerMessage message : messages) { - createMessage(message.target).setType(message.messageType).setPayload(message.message).send(); - } - } - @Override public PlayerMessage createMessage(Target target) { return new PlayerMessage( @@ -454,36 +457,6 @@ import java.util.concurrent.CopyOnWriteArrayList; internalPlayerHandler); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void blockingSendMessages(ExoPlayerMessage... messages) { - List playerMessages = new ArrayList<>(); - for (ExoPlayerMessage message : messages) { - playerMessages.add( - createMessage(message.target) - .setType(message.messageType) - .setPayload(message.message) - .send()); - } - boolean wasInterrupted = false; - for (PlayerMessage message : playerMessages) { - boolean blockMessage = true; - while (blockMessage) { - try { - message.blockUntilDelivered(); - blockMessage = false; - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } - } - @Override public int getCurrentPeriodIndex() { if (shouldMaskPosition()) { @@ -615,11 +588,6 @@ import java.util.concurrent.CopyOnWriteArrayList; return playbackInfo.timeline; } - @Override - public Object getCurrentManifest() { - return playbackInfo.manifest; - } - // 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) { @@ -631,22 +599,26 @@ import java.util.concurrent.CopyOnWriteArrayList; /* positionDiscontinuityReason= */ msg.arg2); break; case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: - PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; - if (!this.playbackParameters.equals(playbackParameters)) { - this.playbackParameters = playbackParameters; - notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters)); - } - break; - case ExoPlayerImplInternal.MSG_ERROR: - ExoPlaybackException playbackError = (ExoPlaybackException) msg.obj; - this.playbackError = playbackError; - notifyListeners(listener -> listener.onPlayerError(playbackError)); + handlePlaybackParameters((PlaybackParameters) msg.obj, /* operationAck= */ msg.arg1 != 0); break; default: throw new IllegalStateException(); } } + private void handlePlaybackParameters( + PlaybackParameters playbackParameters, boolean operationAck) { + if (operationAck) { + pendingSetPlaybackParametersAcks--; + } + if (pendingSetPlaybackParametersAcks == 0) { + if (!this.playbackParameters.equals(playbackParameters)) { + this.playbackParameters = playbackParameters; + notifyListeners(listener -> listener.onPlaybackParametersChanged(playbackParameters)); + } + } + } + private void handlePlaybackInfo( PlaybackInfo playbackInfo, int operationAcks, @@ -657,8 +629,11 @@ import java.util.concurrent.CopyOnWriteArrayList; if (playbackInfo.startPositionUs == C.TIME_UNSET) { // Replace internal unset start position with externally visible start position of zero. playbackInfo = - playbackInfo.resetToNewPosition( - playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs); + playbackInfo.copyWithNewPosition( + playbackInfo.periodId, + /* positionUs= */ 0, + playbackInfo.contentPositionUs, + playbackInfo.totalBufferedDurationUs); } if (!this.playbackInfo.timeline.isEmpty() && playbackInfo.timeline.isEmpty()) { // Update the masking variables, which are used when the timeline becomes empty. @@ -684,7 +659,10 @@ import java.util.concurrent.CopyOnWriteArrayList; } private PlaybackInfo getResetPlaybackInfo( - boolean resetPosition, boolean resetState, int playbackState) { + boolean resetPosition, + boolean resetState, + boolean resetError, + @Player.State int playbackState) { if (resetPosition) { maskingWindowIndex = 0; maskingPeriodIndex = 0; @@ -698,17 +676,17 @@ import java.util.concurrent.CopyOnWriteArrayList; resetPosition = resetPosition || resetState; MediaPeriodId mediaPeriodId = resetPosition - ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window) + ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) : playbackInfo.periodId; long startPositionUs = resetPosition ? 0 : playbackInfo.positionUs; long contentPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs; return new PlaybackInfo( resetState ? Timeline.EMPTY : playbackInfo.timeline, - resetState ? null : playbackInfo.manifest, mediaPeriodId, startPositionUs, contentPositionUs, playbackState, + resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, @@ -781,7 +759,8 @@ import java.util.concurrent.CopyOnWriteArrayList; private final @Player.TimelineChangeReason int timelineChangeReason; private final boolean seekProcessed; private final boolean playbackStateChanged; - private final boolean timelineOrManifestChanged; + private final boolean playbackErrorChanged; + private final boolean timelineChanged; private final boolean isLoadingChanged; private final boolean trackSelectorResultChanged; private final boolean playWhenReady; @@ -808,9 +787,10 @@ import java.util.concurrent.CopyOnWriteArrayList; this.playWhenReady = playWhenReady; this.isPlayingChanged = isPlayingChanged; playbackStateChanged = previousPlaybackInfo.playbackState != playbackInfo.playbackState; - timelineOrManifestChanged = - previousPlaybackInfo.timeline != playbackInfo.timeline - || previousPlaybackInfo.manifest != playbackInfo.manifest; + playbackErrorChanged = + previousPlaybackInfo.playbackError != playbackInfo.playbackError + && playbackInfo.playbackError != null; + timelineChanged = previousPlaybackInfo.timeline != playbackInfo.timeline; isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; trackSelectorResultChanged = previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; @@ -818,18 +798,19 @@ import java.util.concurrent.CopyOnWriteArrayList; @Override public void run() { - if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + if (timelineChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { invokeAll( listenerSnapshot, - listener -> - listener.onTimelineChanged( - playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason)); + listener -> listener.onTimelineChanged(playbackInfo.timeline, timelineChangeReason)); } if (positionDiscontinuity) { invokeAll( listenerSnapshot, listener -> listener.onPositionDiscontinuity(positionDiscontinuityReason)); } + if (playbackErrorChanged) { + invokeAll(listenerSnapshot, listener -> listener.onPlayerError(playbackInfo.playbackError)); + } if (trackSelectorResultChanged) { trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); invokeAll( diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b6ac22059..ddc54e9e6 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -21,14 +21,14 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -51,7 +51,7 @@ import java.util.concurrent.atomic.AtomicBoolean; implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - MediaSource.SourceInfoRefreshListener, + MediaSourceCaller, PlaybackParameterListener, PlayerMessage.Sender { @@ -60,7 +60,6 @@ import java.util.concurrent.atomic.AtomicBoolean; // External messages public static final int MSG_PLAYBACK_INFO_CHANGED = 0; public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1; - public static final int MSG_ERROR = 2; // Internal messages private static final int MSG_PREPARE = 0; @@ -82,8 +81,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 16; private static final int MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL = 17; - private static final int PREPARING_SOURCE_INTERVAL_MS = 10; - private static final int RENDERING_INTERVAL_MS = 10; + private static final int ACTIVE_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; private final Renderer[] renderers; @@ -114,6 +112,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean released; private boolean playWhenReady; private boolean rebuffering; + private boolean shouldContinueLoading; @Player.RepeatMode private int repeatMode; private boolean shuffleModeEnabled; private boolean foregroundMode; @@ -121,7 +120,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; - private int nextPendingMessageIndex; + private int nextPendingMessageIndexHint; + private boolean deliverPendingMessageAtStartPositionRequired; public ExoPlayerImplInternal( Renderer[] renderers, @@ -167,10 +167,11 @@ import java.util.concurrent.atomic.AtomicBoolean; // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. - internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler", - Process.THREAD_PRIORITY_AUDIO); + internalPlaybackThread = + new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); handler = clock.createHandler(internalPlaybackThread.getLooper(), this); + deliverPendingMessageAtStartPositionRequired = true; } public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { @@ -192,7 +193,8 @@ import java.util.concurrent.atomic.AtomicBoolean; } public void seekTo(Timeline timeline, int windowIndex, long positionUs) { - handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) + handler + .obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) .sendToTarget(); } @@ -210,7 +212,7 @@ import java.util.concurrent.atomic.AtomicBoolean; @Override public synchronized void sendMessage(PlayerMessage message) { - if (released) { + if (released || !internalPlaybackThread.isAlive()) { Log.w(TAG, "Ignoring messages sent after release."); message.markAsProcessed(/* isDelivered= */ false); return; @@ -219,6 +221,9 @@ import java.util.concurrent.atomic.AtomicBoolean; } public synchronized void setForegroundMode(boolean foregroundMode) { + if (released || !internalPlaybackThread.isAlive()) { + return; + } if (foregroundMode) { handler.obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 1, 0).sendToTarget(); } else { @@ -227,7 +232,7 @@ import java.util.concurrent.atomic.AtomicBoolean; .obtainMessage(MSG_SET_FOREGROUND_MODE, /* foregroundMode */ 0, 0, processedFlag) .sendToTarget(); boolean wasInterrupted = false; - while (!processedFlag.get() && !released) { + while (!processedFlag.get()) { try { wait(); } catch (InterruptedException e) { @@ -242,7 +247,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } public synchronized void release() { - if (released) { + if (released || !internalPlaybackThread.isAlive()) { return; } handler.sendEmptyMessage(MSG_RELEASE); @@ -264,12 +269,13 @@ import java.util.concurrent.atomic.AtomicBoolean; return internalPlaybackThread.getLooper(); } - // MediaSource.SourceInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { - handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, - new MediaSourceRefreshInfo(source, timeline, manifest)).sendToTarget(); + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { + handler + .obtainMessage(MSG_REFRESH_SOURCE_INFO, new MediaSourceRefreshInfo(source, timeline)) + .sendToTarget(); } // MediaPeriod.Callback implementation. @@ -295,14 +301,11 @@ import java.util.concurrent.atomic.AtomicBoolean; @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - handler - .obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, playbackParameters) - .sendToTarget(); + sendPlaybackParametersChangedInternal(playbackParameters, /* acknowledgeCommand= */ false); } // Handler.Callback implementation. - @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { try { @@ -357,7 +360,8 @@ import java.util.concurrent.atomic.AtomicBoolean; reselectTracksInternal(); break; case MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL: - handlePlaybackParameters((PlaybackParameters) msg.obj); + handlePlaybackParameters( + (PlaybackParameters) msg.obj, /* acknowledgeCommand= */ msg.arg1 != 0); break; case MSG_SEND_MESSAGE: sendMessageInternal((PlayerMessage) msg.obj); @@ -374,32 +378,32 @@ import java.util.concurrent.atomic.AtomicBoolean; } maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { - Log.e(TAG, "Playback error.", e); - eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + Log.e(TAG, getExoPlaybackExceptionMessage(e), e); stopInternal( /* forceResetRenderers= */ true, /* resetPositionAndState= */ false, /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(e); maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { - Log.e(TAG, "Source error.", e); - eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); + Log.e(TAG, "Source error", e); stopInternal( /* forceResetRenderers= */ false, /* resetPositionAndState= */ false, /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e)); maybeNotifyPlaybackInfoChanged(); } catch (RuntimeException | OutOfMemoryError e) { - Log.e(TAG, "Internal runtime error.", e); + Log.e(TAG, "Internal runtime error", e); ExoPlaybackException error = e instanceof OutOfMemoryError ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e) : ExoPlaybackException.createForUnexpected((RuntimeException) e); - eventHandler.obtainMessage(MSG_ERROR, error).sendToTarget(); stopInternal( /* forceResetRenderers= */ true, /* resetPositionAndState= */ false, /* acknowledgeStop= */ false); + playbackInfo = playbackInfo.copyWithPlaybackError(error); maybeNotifyPlaybackInfoChanged(); } return true; @@ -407,18 +411,26 @@ import java.util.concurrent.atomic.AtomicBoolean; // Private methods. + private String getExoPlaybackExceptionMessage(ExoPlaybackException e) { + if (e.type != ExoPlaybackException.TYPE_RENDERER) { + return "Playback error."; + } + return "Renderer error: index=" + + e.rendererIndex + + ", type=" + + Util.getTrackTypeString(renderers[e.rendererIndex].getTrackType()) + + ", format=" + + e.rendererFormat + + ", rendererSupport=" + + RendererCapabilities.getFormatSupportString(e.rendererFormatSupport); + } + private void setState(int state) { if (playbackInfo.playbackState != state) { playbackInfo = playbackInfo.copyWithPlaybackState(state); } } - private void setIsLoading(boolean isLoading) { - if (playbackInfo.isLoading != isLoading) { - playbackInfo = playbackInfo.copyWithIsLoading(isLoading); - } - } - private void maybeNotifyPlaybackInfoChanged() { if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) { eventHandler @@ -437,11 +449,15 @@ import java.util.concurrent.atomic.AtomicBoolean; private void prepareInternal(MediaSource mediaSource, boolean resetPosition, boolean resetState) { pendingPrepareCount++; resetInternal( - /* resetRenderers= */ false, /* releaseMediaSource= */ true, resetPosition, resetState); + /* resetRenderers= */ false, + /* releaseMediaSource= */ true, + resetPosition, + resetState, + /* resetError= */ true); loadControl.onPrepared(); this.mediaSource = mediaSource; setState(Player.STATE_BUFFERING); - mediaSource.prepareSource(/* listener= */ this, bandwidthMeter.getTransferListener()); + mediaSource.prepareSource(/* caller= */ this, bandwidthMeter.getTransferListener()); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -486,12 +502,7 @@ import java.util.concurrent.atomic.AtomicBoolean; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); if (newPositionUs != playbackInfo.positionUs) { - playbackInfo = - playbackInfo.copyWithNewPosition( - periodId, - newPositionUs, - playbackInfo.contentPositionUs, - getTotalBufferedDurationUs()); + playbackInfo = copyWithNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); if (sendDiscontinuity) { playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } @@ -514,29 +525,31 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void updatePlaybackPositions() throws ExoPlaybackException { - if (!queue.hasPlayingPeriod()) { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder == null) { return; } // Update the playback position. - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); - if (periodPositionUs != C.TIME_UNSET) { - resetRendererPosition(periodPositionUs); + long discontinuityPositionUs = + playingPeriodHolder.prepared + ? playingPeriodHolder.mediaPeriod.readDiscontinuity() + : C.TIME_UNSET; + if (discontinuityPositionUs != C.TIME_UNSET) { + resetRendererPosition(discontinuityPositionUs); // A MediaPeriod may report a discontinuity at the current playback position to ensure the // renderers are flushed. Only report the discontinuity externally if the position changed. - if (periodPositionUs != playbackInfo.positionUs) { + if (discontinuityPositionUs != playbackInfo.positionUs) { playbackInfo = - playbackInfo.copyWithNewPosition( - playbackInfo.periodId, - periodPositionUs, - playbackInfo.contentPositionUs, - getTotalBufferedDurationUs()); + copyWithNewPosition( + playbackInfo.periodId, discontinuityPositionUs, playbackInfo.contentPositionUs); playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } else { - rendererPositionUs = mediaClock.syncAndGetPositionUs(); - periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + rendererPositionUs = + mediaClock.syncAndGetPositionUs( + /* isReadingAhead= */ playingPeriodHolder != queue.getReadingPeriod()); + long periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs); playbackInfo.positionUs = periodPositionUs; } @@ -550,60 +563,78 @@ import java.util.concurrent.atomic.AtomicBoolean; private void doSomeWork() throws ExoPlaybackException, IOException { long operationStartTimeMs = clock.uptimeMillis(); updatePeriods(); - if (!queue.hasPlayingPeriod()) { - // We're still waiting for the first period to be prepared. - maybeThrowPeriodPrepareError(); - scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); + + if (playbackInfo.playbackState == Player.STATE_IDLE + || playbackInfo.playbackState == Player.STATE_ENDED) { + // Remove all messages. Prepare (in case of IDLE) or seek (in case of ENDED) will resume. + handler.removeMessages(MSG_DO_SOME_WORK); + return; + } + + @Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder == null) { + // We're still waiting until the playing period is available. + scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); return; } - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); - long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; - - playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs, - retainBackBufferFromKeyframe); boolean renderersEnded = true; - boolean renderersReadyOrEnded = true; - for (Renderer renderer : enabledRenderers) { - // TODO: Each renderer should return the maximum delay before which it wishes to be called - // again. The minimum of these values should then be used as the delay before the next - // invocation of this method. - renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); - renderersEnded = renderersEnded && renderer.isEnded(); - // Determine whether the renderer is ready (or ended). We override to assume the renderer is - // ready if it needs the next sample stream. This is necessary to avoid getting stuck if - // tracks in the current period have uneven durations. See: - // https://github.com/google/ExoPlayer/issues/1874 - boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded() - || rendererWaitingForNextStream(renderer); - if (!rendererReadyOrEnded) { - renderer.maybeThrowStreamError(); + boolean renderersAllowPlayback = true; + if (playingPeriodHolder.prepared) { + long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; + playingPeriodHolder.mediaPeriod.discardBuffer( + playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + if (renderer.getState() == Renderer.STATE_DISABLED) { + continue; + } + // TODO: Each renderer should return the maximum delay before which it wishes to be called + // again. The minimum of these values should then be used as the delay before the next + // invocation of this method. + renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); + renderersEnded = renderersEnded && renderer.isEnded(); + // Determine whether the renderer allows playback to continue. Playback can continue if the + // renderer is ready or ended. Also continue playback if the renderer is reading ahead into + // the next stream or is waiting for the next stream. This is to avoid getting stuck if + // tracks in the current period have uneven durations and are still being read by another + // renderer. See: https://github.com/google/ExoPlayer/issues/1874. + boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream(); + boolean isWaitingForNextStream = + !isReadingAhead + && playingPeriodHolder.getNext() != null + && renderer.hasReadStreamToEnd(); + boolean allowsPlayback = + isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded(); + renderersAllowPlayback = renderersAllowPlayback && allowsPlayback; + if (!allowsPlayback) { + renderer.maybeThrowStreamError(); + } } - renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded; - } - if (!renderersReadyOrEnded) { - maybeThrowPeriodPrepareError(); + } else { + playingPeriodHolder.mediaPeriod.maybeThrowPrepareError(); } long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; if (renderersEnded + && playingPeriodHolder.prepared && (playingPeriodDurationUs == C.TIME_UNSET || playingPeriodDurationUs <= playbackInfo.positionUs) && playingPeriodHolder.info.isFinal) { setState(Player.STATE_ENDED); stopRenderers(); } else if (playbackInfo.playbackState == Player.STATE_BUFFERING - && shouldTransitionToReadyState(renderersReadyOrEnded)) { + && shouldTransitionToReadyState(renderersAllowPlayback)) { setState(Player.STATE_READY); if (playWhenReady) { startRenderers(); } } else if (playbackInfo.playbackState == Player.STATE_READY - && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersReadyOrEnded)) { + && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersAllowPlayback)) { rebuffering = playWhenReady; setState(Player.STATE_BUFFERING); stopRenderers(); @@ -617,7 +648,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) || playbackInfo.playbackState == Player.STATE_BUFFERING) { - scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); + scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS); } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { @@ -644,7 +675,7 @@ import java.util.concurrent.atomic.AtomicBoolean; if (resolvedSeekPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed or is not ready and a suitable seek position could not be resolved. - periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window); + periodId = playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period); periodPositionUs = C.TIME_UNSET; contentPositionUs = C.TIME_UNSET; seekPositionAdjusted = true; @@ -673,13 +704,16 @@ import java.util.concurrent.atomic.AtomicBoolean; /* resetRenderers= */ false, /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* resetState= */ false); + /* resetState= */ false, + /* resetError= */ true); } else { // Execute the seek in the current media periods. long newPeriodPositionUs = periodPositionUs; if (periodId.equals(playbackInfo.periodId)) { MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - if (playingPeriodHolder != null && newPeriodPositionUs != 0) { + if (playingPeriodHolder != null + && playingPeriodHolder.prepared + && newPeriodPositionUs != 0) { newPeriodPositionUs = playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( newPeriodPositionUs, seekParameters); @@ -695,9 +729,7 @@ import java.util.concurrent.atomic.AtomicBoolean; periodPositionUs = newPeriodPositionUs; } } finally { - playbackInfo = - playbackInfo.copyWithNewPosition( - periodId, periodPositionUs, contentPositionUs, getTotalBufferedDurationUs()); + playbackInfo = copyWithNewPosition(periodId, periodPositionUs, contentPositionUs); if (seekPositionAdjusted) { playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); } @@ -716,7 +748,9 @@ import java.util.concurrent.atomic.AtomicBoolean; throws ExoPlaybackException { stopRenderers(); rebuffering = false; - setState(Player.STATE_BUFFERING); + if (playbackInfo.playbackState != Player.STATE_IDLE && !playbackInfo.timeline.isEmpty()) { + setState(Player.STATE_BUFFERING); + } // Clear the timeline, but keep the requested period if it is already prepared. MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); @@ -769,10 +803,11 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { + MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod(); rendererPositionUs = - !queue.hasPlayingPeriod() + playingMediaPeriod == null ? periodPositionUs - : queue.getPlayingPeriod().toRendererTime(periodPositionUs); + : playingMediaPeriod.toRendererTime(periodPositionUs); mediaClock.resetPosition(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); @@ -782,6 +817,8 @@ import java.util.concurrent.atomic.AtomicBoolean; private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { mediaClock.setPlaybackParameters(playbackParameters); + sendPlaybackParametersChangedInternal( + mediaClock.getPlaybackParameters(), /* acknowledgeCommand= */ true); } private void setSeekParametersInternal(SeekParameters seekParameters) { @@ -814,7 +851,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* resetRenderers= */ forceResetRenderers || !foregroundMode, /* releaseMediaSource= */ true, /* resetPosition= */ resetPositionAndState, - /* resetState= */ resetPositionAndState); + /* resetState= */ resetPositionAndState, + /* resetError= */ resetPositionAndState); playbackInfoUpdate.incrementPendingOperationAcks( pendingPrepareCount + (acknowledgeStop ? 1 : 0)); pendingPrepareCount = 0; @@ -827,7 +865,8 @@ import java.util.concurrent.atomic.AtomicBoolean; /* resetRenderers= */ true, /* releaseMediaSource= */ true, /* resetPosition= */ true, - /* resetState= */ true); + /* resetState= */ true, + /* resetError= */ false); loadControl.onReleased(); setState(Player.STATE_IDLE); internalPlaybackThread.quit(); @@ -841,7 +880,8 @@ import java.util.concurrent.atomic.AtomicBoolean; boolean resetRenderers, boolean releaseMediaSource, boolean resetPosition, - boolean resetState) { + boolean resetState, + boolean resetError) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); @@ -880,19 +920,18 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - queue.clear(/* keepFrontPeriodUid= */ !resetPosition); - setIsLoading(false); + queue.clear(/* keepFrontPeriodUid= */ !resetState); + shouldContinueLoading = false; if (resetState) { queue.setTimeline(Timeline.EMPTY); for (PendingMessageInfo pendingMessageInfo : pendingMessages) { pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } pendingMessages.clear(); - nextPendingMessageIndex = 0; } MediaPeriodId mediaPeriodId = resetPosition - ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window) + ? playbackInfo.getDummyFirstMediaPeriodId(shuffleModeEnabled, window, period) : playbackInfo.periodId; // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. long startPositionUs = resetPosition ? C.TIME_UNSET : playbackInfo.positionUs; @@ -900,11 +939,11 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = new PlaybackInfo( resetState ? Timeline.EMPTY : playbackInfo.timeline, - resetState ? null : playbackInfo.manifest, mediaPeriodId, startPositionUs, contentPositionUs, playbackInfo.playbackState, + resetError ? null : playbackInfo.playbackError, /* isLoading= */ false, resetState ? TrackGroupArray.EMPTY : playbackInfo.trackGroups, resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult, @@ -914,7 +953,12 @@ import java.util.concurrent.atomic.AtomicBoolean; startPositionUs); if (releaseMediaSource) { if (mediaSource != null) { - mediaSource.releaseSource(/* listener= */ this); + try { + mediaSource.releaseSource(/* caller= */ this); + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Failed to release child source.", e); + } mediaSource = null; } } @@ -954,6 +998,11 @@ import java.util.concurrent.atomic.AtomicBoolean; private void sendMessageToTargetThread(final PlayerMessage message) { Handler handler = message.getHandler(); + if (!handler.getLooper().getThread().isAlive()) { + Log.w("TAG", "Trying to send message on a dead thread."); + message.markAsProcessed(/* isDelivered= */ false); + return; + } handler.post( () -> { try { @@ -1022,13 +1071,17 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } // If this is the first call from the start position, include oldPeriodPositionUs in potential - // trigger positions. - if (playbackInfo.startPositionUs == oldPeriodPositionUs) { + // trigger positions, but make sure we deliver it only once. + if (playbackInfo.startPositionUs == oldPeriodPositionUs + && deliverPendingMessageAtStartPositionRequired) { oldPeriodPositionUs--; } + deliverPendingMessageAtStartPositionRequired = false; + // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) int currentPeriodIndex = playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid); + int nextPendingMessageIndex = Math.min(nextPendingMessageIndexHint, pendingMessages.size()); PendingMessageInfo previousInfo = nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null; while (previousInfo != null @@ -1074,6 +1127,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ? pendingMessages.get(nextPendingMessageIndex) : null; } + nextPendingMessageIndexHint = nextPendingMessageIndex; } private void ensureStopped(Renderer renderer) throws ExoPlaybackException { @@ -1089,10 +1143,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void reselectTracksInternal() throws ExoPlaybackException { - if (!queue.hasPlayingPeriod()) { - // We don't have tracks yet, so we don't care. - return; - } float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); @@ -1105,7 +1155,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } newTrackSelectorResult = periodHolder.selectTracks(playbackSpeed, playbackInfo.timeline); - if (newTrackSelectorResult != null) { + if (!newTrackSelectorResult.isEquivalent(periodHolder.getTrackSelectorResult())) { // Selected tracks have changed for this period. break; } @@ -1128,11 +1178,8 @@ import java.util.concurrent.atomic.AtomicBoolean; if (playbackInfo.playbackState != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { playbackInfo = - playbackInfo.copyWithNewPosition( - playbackInfo.periodId, - periodPositionUs, - playbackInfo.contentPositionUs, - getTotalBufferedDurationUs()); + copyWithNewPosition( + playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); resetRendererPosition(periodPositionUs); } @@ -1179,8 +1226,8 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { - MediaPeriodHolder periodHolder = queue.getFrontPeriod(); - while (periodHolder != null && periodHolder.prepared) { + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + while (periodHolder != null) { TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); for (TrackSelection trackSelection : trackSelections) { if (trackSelection != null) { @@ -1192,15 +1239,12 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void notifyTrackSelectionDiscontinuity() { - MediaPeriodHolder periodHolder = queue.getFrontPeriod(); + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); while (periodHolder != null) { - TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult(); - if (trackSelectorResult != null) { - TrackSelection[] trackSelections = trackSelectorResult.selections.getAll(); - for (TrackSelection trackSelection : trackSelections) { - if (trackSelection != null) { - trackSelection.onDiscontinuity(); - } + TrackSelection[] trackSelections = periodHolder.getTrackSelectorResult().selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + if (trackSelection != null) { + trackSelection.onDiscontinuity(); } } periodHolder = periodHolder.getNext(); @@ -1230,12 +1274,10 @@ import java.util.concurrent.atomic.AtomicBoolean; private boolean isTimelineReady() { MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - MediaPeriodHolder nextPeriodHolder = playingPeriodHolder.getNext(); long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; - return playingPeriodDurationUs == C.TIME_UNSET - || playbackInfo.positionUs < playingPeriodDurationUs - || (nextPeriodHolder != null - && (nextPeriodHolder.prepared || nextPeriodHolder.info.id.isAd())); + return playingPeriodHolder.prepared + && (playingPeriodDurationUs == C.TIME_UNSET + || playbackInfo.positionUs < playingPeriodDurationUs); } private void maybeThrowSourceInfoRefreshError() throws IOException { @@ -1251,21 +1293,6 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaSource.maybeThrowSourceInfoRefreshError(); } - private void maybeThrowPeriodPrepareError() throws IOException { - MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - if (loadingPeriodHolder != null - && !loadingPeriodHolder.prepared - && (readingPeriodHolder == null || readingPeriodHolder.getNext() == loadingPeriodHolder)) { - for (Renderer renderer : enabledRenderers) { - if (!renderer.hasReadStreamToEnd()) { - return; - } - } - loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError(); - } - } - private void handleSourceInfoRefreshed(MediaSourceRefreshInfo sourceRefreshInfo) throws ExoPlaybackException { if (sourceRefreshInfo.source != mediaSource) { @@ -1277,9 +1304,8 @@ import java.util.concurrent.atomic.AtomicBoolean; Timeline oldTimeline = playbackInfo.timeline; Timeline timeline = sourceRefreshInfo.timeline; - Object manifest = sourceRefreshInfo.manifest; queue.setTimeline(timeline); - playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); + playbackInfo = playbackInfo.copyWithTimeline(timeline); resolvePendingMessagePositions(); MediaPeriodId newPeriodId = playbackInfo.periodId; @@ -1343,7 +1369,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } else { // Something changed. Seek to new start position. - MediaPeriodHolder periodHolder = queue.getFrontPeriod(); + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); if (periodHolder != null) { // Update the new playing media period info if it already exists. while (periodHolder.getNext() != null) { @@ -1356,9 +1382,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // Actually do the seek. long newPositionUs = newPeriodId.isAd() ? 0 : newContentPositionUs; long seekedToPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs); - playbackInfo = - playbackInfo.copyWithNewPosition( - newPeriodId, seekedToPositionUs, newContentPositionUs, getTotalBufferedDurationUs()); + playbackInfo = copyWithNewPosition(newPeriodId, seekedToPositionUs, newContentPositionUs); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } @@ -1369,6 +1393,9 @@ import java.util.concurrent.atomic.AtomicBoolean; return 0; } long maxReadPositionUs = readingHolder.getRendererOffset(); + if (!readingHolder.prepared) { + return maxReadPositionUs; + } for (int i = 0; i < renderers.length; i++) { if (renderers[i].getState() == Renderer.STATE_DISABLED || renderers[i].getStream() != readingHolder.sampleStreams[i]) { @@ -1386,13 +1413,16 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void handleSourceInfoRefreshEndedPlayback() { - setState(Player.STATE_ENDED); + if (playbackInfo.playbackState != Player.STATE_IDLE) { + setState(Player.STATE_ENDED); + } // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* resetRenderers= */ false, /* releaseMediaSource= */ false, /* resetPosition= */ true, - /* resetState= */ false); + /* resetState= */ false, + /* resetError= */ true); } /** @@ -1411,8 +1441,9 @@ import java.util.concurrent.atomic.AtomicBoolean; int newPeriodIndex = C.INDEX_UNSET; int maxIterations = oldTimeline.getPeriodCount(); for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { - oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode, - shuffleModeEnabled); + oldPeriodIndex = + oldTimeline.getNextPeriodIndex( + oldPeriodIndex, period, window, repeatMode, shuffleModeEnabled); if (oldPeriodIndex == C.INDEX_UNSET) { // We've reached the end of the old timeline. break; @@ -1433,6 +1464,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ + @Nullable private Pair resolveSeekPosition( SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; @@ -1449,8 +1481,9 @@ import java.util.concurrent.atomic.AtomicBoolean; // Map the SeekPosition to a position in the corresponding timeline. Pair periodPosition; try { - periodPosition = seekTimeline.getPeriodPosition(window, period, seekPosition.windowIndex, - seekPosition.windowPositionUs); + periodPosition = + seekTimeline.getPeriodPosition( + window, period, seekPosition.windowIndex, seekPosition.windowPositionUs); } catch (IndexOutOfBoundsException e) { // The window index of the seek position was outside the bounds of the timeline. return null; @@ -1467,11 +1500,12 @@ import java.util.concurrent.atomic.AtomicBoolean; } if (trySubsequentPeriods) { // Try and find a subsequent period from the seek timeline in the internal timeline. + @Nullable Object periodUid = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); if (periodUid != null) { - // We found one. Map the SeekPosition onto the corresponding default position. + // We found one. Use the default position of the corresponding window. return getPeriodPosition( - timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + timeline, timeline.getPeriodByUid(periodUid, period).windowIndex, C.TIME_UNSET); } } // We didn't find one. Give up. @@ -1497,85 +1531,71 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaSource.maybeThrowSourceInfoRefreshError(); return; } - - // Update the loading period if required. maybeUpdateLoadingPeriod(); - MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { - setIsLoading(false); - } else if (!playbackInfo.isLoading) { + maybeUpdateReadingPeriod(); + maybeUpdatePlayingPeriod(); + } + + private void maybeUpdateLoadingPeriod() throws ExoPlaybackException, IOException { + queue.reevaluateBuffer(rendererPositionUs); + if (queue.shouldLoadNextMediaPeriod()) { + MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); + if (info == null) { + maybeThrowSourceInfoRefreshError(); + } else { + MediaPeriodHolder mediaPeriodHolder = + queue.enqueueNextMediaPeriodHolder( + rendererCapabilities, + trackSelector, + loadControl.getAllocator(), + mediaSource, + info, + emptyTrackSelectorResult); + mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); + if (queue.getPlayingPeriod() == mediaPeriodHolder) { + resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime()); + } + handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + } + } + if (shouldContinueLoading) { + shouldContinueLoading = isLoadingPossible(); + updateIsLoading(); + } else { maybeContinueLoading(); } + } - if (!queue.hasPlayingPeriod()) { - // We're waiting for the first period to be prepared. + private void maybeUpdateReadingPeriod() throws ExoPlaybackException { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (readingPeriodHolder == null) { return; } - // Advance the playing period if necessary. - MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - boolean advancedPlayingPeriod = false; - while (playWhenReady - && playingPeriodHolder != readingPeriodHolder - && rendererPositionUs >= playingPeriodHolder.getNext().getStartPositionRendererTime()) { - // All enabled renderers' streams have been read to the end, and the playback position reached - // the end of the playing period, so advance playback to the next period. - if (advancedPlayingPeriod) { - // If we advance more than one period at a time, notify listeners after each update. - maybeNotifyPlaybackInfoChanged(); - } - int discontinuityReason = - playingPeriodHolder.info.isLastInTimelinePeriod - ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION - : Player.DISCONTINUITY_REASON_AD_INSERTION; - MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder; - playingPeriodHolder = queue.advancePlayingPeriod(); - updatePlayingPeriodRenderers(oldPlayingPeriodHolder); - playbackInfo = - playbackInfo.copyWithNewPosition( - playingPeriodHolder.info.id, - playingPeriodHolder.info.startPositionUs, - playingPeriodHolder.info.contentPositionUs, - getTotalBufferedDurationUs()); - playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); - updatePlaybackPositions(); - advancedPlayingPeriod = true; - } - - if (readingPeriodHolder.info.isFinal) { - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; - // Defer setting the stream as final until the renderer has actually consumed the whole - // stream in case of playlist changes that cause the stream to be no longer final. - if (sampleStream != null && renderer.getStream() == sampleStream - && renderer.hasReadStreamToEnd()) { - renderer.setCurrentStreamFinal(); + if (readingPeriodHolder.getNext() == null) { + // We don't have a successor to advance the reading period to. + if (readingPeriodHolder.info.isFinal) { + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; + // Defer setting the stream as final until the renderer has actually consumed the whole + // stream in case of playlist changes that cause the stream to be no longer final. + if (sampleStream != null + && renderer.getStream() == sampleStream + && renderer.hasReadStreamToEnd()) { + renderer.setCurrentStreamFinal(); + } } } return; } - // Advance the reading period if necessary. - if (readingPeriodHolder.getNext() == null) { - // We don't have a successor to advance the reading period to. + if (!hasReadingPeriodFinishedReading()) { return; } - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; - if (renderer.getStream() != sampleStream - || (sampleStream != null && !renderer.hasReadStreamToEnd())) { - // The current reading period is still being read by at least one renderer. - return; - } - } - if (!readingPeriodHolder.getNext().prepared) { // The successor is not prepared yet. - maybeThrowPeriodPrepareError(); return; } @@ -1583,18 +1603,18 @@ import java.util.concurrent.atomic.AtomicBoolean; readingPeriodHolder = queue.advanceReadingPeriod(); TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult(); - boolean initialDiscontinuity = - readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; + if (readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { + // The new period starts with a discontinuity, so the renderers will play out all data, then + // be disabled and re-enabled when they start playing the next period. + setAllRendererStreamsFinal(); + return; + } for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; boolean rendererWasEnabled = oldTrackSelectorResult.isRendererEnabled(i); - if (!rendererWasEnabled) { - // The renderer was disabled and will be enabled when we play the next period. - } else if (initialDiscontinuity) { - // The new period starts with a discontinuity, so the renderer will play out all data then - // be disabled and re-enabled when it starts playing the next period. - renderer.setCurrentStreamFinal(); - } else if (!renderer.isCurrentStreamFinal()) { + if (rendererWasEnabled && !renderer.isCurrentStreamFinal()) { + // The renderer is enabled and its stream is not final, so we still have a chance to replace + // the sample streams. TrackSelection newSelection = newTrackSelectorResult.selections.get(i); boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; @@ -1608,7 +1628,9 @@ import java.util.concurrent.atomic.AtomicBoolean; // and it will change the provided rendererOffsetUs while the renderer is still // rendering from the playing media period. Format[] formats = getFormats(newSelection); - renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], + renderer.replaceStream( + formats, + readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); } else { // The renderer will be disabled when transitioning to playing the next period, because @@ -1622,23 +1644,76 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private void maybeUpdateLoadingPeriod() throws IOException { - queue.reevaluateBuffer(rendererPositionUs); - if (queue.shouldLoadNextMediaPeriod()) { - MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo); - if (info == null) { - maybeThrowSourceInfoRefreshError(); - } else { - MediaPeriod mediaPeriod = - queue.enqueueNextMediaPeriod( - rendererCapabilities, - trackSelector, - loadControl.getAllocator(), - mediaSource, - info); - mediaPeriod.prepare(this, info.startPositionUs); - setIsLoading(true); - handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); + private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { + boolean advancedPlayingPeriod = false; + while (shouldAdvancePlayingPeriod()) { + if (advancedPlayingPeriod) { + // If we advance more than one period at a time, notify listeners after each update. + maybeNotifyPlaybackInfoChanged(); + } + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + if (oldPlayingPeriodHolder == queue.getReadingPeriod()) { + // The reading period hasn't advanced yet, so we can't seamlessly replace the SampleStreams + // anymore and need to re-enable the renderers. Set all current streams final to do that. + setAllRendererStreamsFinal(); + } + MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod(); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); + playbackInfo = + copyWithNewPosition( + newPlayingPeriodHolder.info.id, + newPlayingPeriodHolder.info.startPositionUs, + newPlayingPeriodHolder.info.contentPositionUs); + int discontinuityReason = + oldPlayingPeriodHolder.info.isLastInTimelinePeriod + ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + : Player.DISCONTINUITY_REASON_AD_INSERTION; + playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); + updatePlaybackPositions(); + advancedPlayingPeriod = true; + } + } + + private boolean shouldAdvancePlayingPeriod() { + if (!playWhenReady) { + return false; + } + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + if (playingPeriodHolder == null) { + return false; + } + MediaPeriodHolder nextPlayingPeriodHolder = playingPeriodHolder.getNext(); + if (nextPlayingPeriodHolder == null) { + return false; + } + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (playingPeriodHolder == readingPeriodHolder && !hasReadingPeriodFinishedReading()) { + return false; + } + return rendererPositionUs >= nextPlayingPeriodHolder.getStartPositionRendererTime(); + } + + private boolean hasReadingPeriodFinishedReading() { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); + if (!readingPeriodHolder.prepared) { + return false; + } + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; + if (renderer.getStream() != sampleStream + || (sampleStream != null && !renderer.hasReadStreamToEnd())) { + // The current reading period is still being read by at least one renderer. + return false; + } + } + return true; + } + + private void setAllRendererStreamsFinal() { + for (Renderer renderer : renderers) { + if (renderer.getStream() != null) { + renderer.setCurrentStreamFinal(); } } } @@ -1653,10 +1728,9 @@ import java.util.concurrent.atomic.AtomicBoolean; mediaClock.getPlaybackParameters().speed, playbackInfo.timeline); updateLoadControlTrackSelection( loadingPeriodHolder.getTrackGroups(), loadingPeriodHolder.getTrackSelectorResult()); - if (!queue.hasPlayingPeriod()) { - // This is the first prepared period, so start playing it. - MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod(); - resetRendererPosition(playingPeriodHolder.info.startPositionUs); + if (loadingPeriodHolder == queue.getPlayingPeriod()) { + // This is the first prepared period, so update the position and the renderers. + resetRendererPosition(loadingPeriodHolder.info.startPositionUs); updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null); } maybeContinueLoading(); @@ -1671,9 +1745,13 @@ import java.util.concurrent.atomic.AtomicBoolean; maybeContinueLoading(); } - private void handlePlaybackParameters(PlaybackParameters playbackParameters) + private void handlePlaybackParameters( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) throws ExoPlaybackException { - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + eventHandler + .obtainMessage( + MSG_PLAYBACK_PARAMETERS_CHANGED, acknowledgeCommand ? 1 : 0, 0, playbackParameters) + .sendToTarget(); updateTrackSelectionPlaybackSpeed(playbackParameters.speed); for (Renderer renderer : renderers) { if (renderer != null) { @@ -1683,21 +1761,49 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void maybeContinueLoading() { - MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); - long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs(); - if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { - setIsLoading(false); - return; + shouldContinueLoading = shouldContinueLoading(); + if (shouldContinueLoading) { + queue.getLoadingPeriod().continueLoading(rendererPositionUs); + } + updateIsLoading(); + } + + private boolean shouldContinueLoading() { + if (!isLoadingPossible()) { + return false; } long bufferedDurationUs = - getTotalBufferedDurationUs(/* bufferedPositionInLoadingPeriodUs= */ nextLoadPositionUs); - boolean continueLoading = - loadControl.shouldContinueLoading( - bufferedDurationUs, mediaClock.getPlaybackParameters().speed); - setIsLoading(continueLoading); - if (continueLoading) { - loadingPeriodHolder.continueLoading(rendererPositionUs); + getTotalBufferedDurationUs(queue.getLoadingPeriod().getNextLoadPositionUs()); + float playbackSpeed = mediaClock.getPlaybackParameters().speed; + return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); + } + + private boolean isLoadingPossible() { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + if (loadingPeriodHolder == null) { + return false; } + long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs(); + if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { + return false; + } + return true; + } + + private void updateIsLoading() { + MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod(); + boolean isLoading = + shouldContinueLoading || (loadingPeriod != null && loadingPeriod.mediaPeriod.isLoading()); + if (isLoading != playbackInfo.isLoading) { + playbackInfo = playbackInfo.copyWithIsLoading(isLoading); + } + } + + private PlaybackInfo copyWithNewPosition( + MediaPeriodId mediaPeriodId, long positionUs, long contentPositionUs) { + deliverPendingMessageAtStartPositionRequired = true; + return playbackInfo.copyWithNewPosition( + mediaPeriodId, positionUs, contentPositionUs, getTotalBufferedDurationUs()); } @SuppressWarnings("ParameterNotNullable") @@ -1769,9 +1875,13 @@ import java.util.concurrent.atomic.AtomicBoolean; // Consider as joining only if the renderer was previously disabled. boolean joining = !wasRendererEnabled && playing; // Enable the renderer. - renderer.enable(rendererConfiguration, formats, - playingPeriodHolder.sampleStreams[rendererIndex], rendererPositionUs, - joining, playingPeriodHolder.getRendererOffset()); + renderer.enable( + rendererConfiguration, + formats, + playingPeriodHolder.sampleStreams[rendererIndex], + rendererPositionUs, + joining, + playingPeriodHolder.getRendererOffset()); mediaClock.onRendererEnabled(renderer); // Start the renderer if playing. if (playing) { @@ -1780,12 +1890,6 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - private boolean rendererWaitingForNextStream(Renderer renderer) { - MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); - MediaPeriodHolder nextPeriodHolder = readingPeriodHolder.getNext(); - return nextPeriodHolder != null && nextPeriodHolder.prepared && renderer.hasReadStreamToEnd(); - } - private void handleLoadingMediaPeriodChanged(boolean loadingTrackSelectionChanged) { MediaPeriodHolder loadingMediaPeriodHolder = queue.getLoadingPeriod(); MediaPeriodId loadingMediaPeriodId = @@ -1828,6 +1932,17 @@ import java.util.concurrent.atomic.AtomicBoolean; loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections); } + private void sendPlaybackParametersChangedInternal( + PlaybackParameters playbackParameters, boolean acknowledgeCommand) { + handler + .obtainMessage( + MSG_PLAYBACK_PARAMETERS_CHANGED_INTERNAL, + acknowledgeCommand ? 1 : 0, + 0, + playbackParameters) + .sendToTarget(); + } + private static Format[] getFormats(TrackSelection newSelection) { // Build an array of formats contained by the selection. int length = newSelection != null ? newSelection.length() : 0; @@ -1857,7 +1972,7 @@ import java.util.concurrent.atomic.AtomicBoolean; public int resolvedPeriodIndex; public long resolvedPeriodTimeUs; - public @Nullable Object resolvedPeriodUid; + @Nullable public Object resolvedPeriodUid; public PendingMessageInfo(PlayerMessage message) { this.message = message; @@ -1870,7 +1985,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } @Override - public int compareTo(@NonNull PendingMessageInfo other) { + public int compareTo(PendingMessageInfo other) { if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { // PendingMessageInfos with a resolved period position are always smaller. return resolvedPeriodUid != null ? -1 : 1; @@ -1892,12 +2007,10 @@ import java.util.concurrent.atomic.AtomicBoolean; public final MediaSource source; public final Timeline timeline; - public final Object manifest; - public MediaSourceRefreshInfo(MediaSource source, Timeline timeline, Object manifest) { + public MediaSourceRefreshInfo(MediaSource source, Timeline timeline) { this.source = source; this.timeline = timeline; - this.manifest = manifest; } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index adc05eb20..35b6199cd 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.10.6"; + public static final String VERSION = "2.11.7"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.6"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.7"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2010006; + public static final int VERSION_INT = 2011007; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/Format.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/Format.java index 85b8230d6..19ed34405 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/Format.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/Format.java @@ -19,6 +19,8 @@ import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -45,9 +47,9 @@ public final class Format implements Parcelable { public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; /** An identifier for the format, or null if unknown or not applicable. */ - public final @Nullable String id; + @Nullable public final String id; /** The human readable label, or null if unknown or not applicable. */ - public final @Nullable String label; + @Nullable public final String label; /** Track selection flags. */ @C.SelectionFlags public final int selectionFlags; /** Track role flags. */ @@ -57,14 +59,14 @@ public final class Format implements Parcelable { */ public final int bitrate; /** Codecs of the format as described in RFC 6381, or null if unknown or not applicable. */ - public final @Nullable String codecs; + @Nullable public final String codecs; /** Metadata, or null if unknown or not applicable. */ - public final @Nullable Metadata metadata; + @Nullable public final Metadata metadata; // Container specific. /** The mime type of the container, or null if unknown or not applicable. */ - public final @Nullable String containerMimeType; + @Nullable public final String containerMimeType; // Elementary stream specific. @@ -72,7 +74,7 @@ public final class Format implements Parcelable { * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not * applicable. */ - public final @Nullable String sampleMimeType; + @Nullable public final String sampleMimeType; /** * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or * not applicable. @@ -84,7 +86,7 @@ public final class Format implements Parcelable { */ public final List initializationData; /** DRM initialization data if the stream is protected, or null otherwise. */ - public final @Nullable DrmInitData drmInitData; + @Nullable public final DrmInitData drmInitData; /** * For samples that contain subsamples, this is an offset that should be added to subsample @@ -122,9 +124,9 @@ public final class Format implements Parcelable { @C.StereoMode public final int stereoMode; /** The projection data for 360/VR video, or null if not applicable. */ - public final @Nullable byte[] projectionData; + @Nullable public final byte[] projectionData; /** The color metadata associated with the video, helps with accurate color reproduction. */ - public final @Nullable ColorInfo colorInfo; + @Nullable public final ColorInfo colorInfo; // Audio specific. @@ -136,13 +138,7 @@ public final class Format implements Parcelable { * 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 #sampleMimeType} is {@link MimeTypes#AUDIO_RAW} - * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link - * C#ENCODING_PCM_24BIT}, {@link C#ENCODING_PCM_32BIT}, {@link C#ENCODING_PCM_FLOAT}, {@link - * C#ENCODING_PCM_MU_LAW} or {@link C#ENCODING_PCM_A_LAW}. Set to {@link #NO_VALUE} for other - * media types. - */ + /** The {@link C.PcmEncoding} for PCM audio. Set to {@link #NO_VALUE} for other media types. */ public final @C.PcmEncoding int pcmEncoding; /** * The number of frames to trim from the start of the decoded audio stream, or 0 if not @@ -157,12 +153,21 @@ public final class Format implements Parcelable { // Audio and text specific. /** The language as an IETF BCP 47 conformant tag, or null if unknown or not applicable. */ - public final @Nullable String language; + @Nullable public final String language; /** * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ public final int accessibilityChannel; + // Provided by source. + + /** + * The type of the {@link ExoMediaCrypto} provided by the media source, if the media source can + * acquire a {@link DrmSession} for {@link #drmInitData}. Null if the media source cannot acquire + * a session for {@link #drmInitData}, or if not applicable. + */ + @Nullable public final Class exoMediaCryptoType; + // Lazily initialized hashcode. private int hashCode; @@ -176,8 +181,8 @@ public final class Format implements Parcelable { public static Format createVideoContainerFormat( @Nullable String id, @Nullable String containerMimeType, - String sampleMimeType, - String codecs, + @Nullable String sampleMimeType, + @Nullable String codecs, int bitrate, int width, int height, @@ -204,8 +209,8 @@ public final class Format implements Parcelable { @Nullable String id, @Nullable String label, @Nullable String containerMimeType, - String sampleMimeType, - String codecs, + @Nullable String sampleMimeType, + @Nullable String codecs, @Nullable Metadata metadata, int bitrate, int width, @@ -242,7 +247,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createVideoSampleFormat( @@ -314,7 +320,7 @@ public final class Format implements Parcelable { @Nullable List initializationData, int rotationDegrees, float pixelWidthHeightRatio, - byte[] projectionData, + @Nullable byte[] projectionData, @C.StereoMode int stereoMode, @Nullable ColorInfo colorInfo, @Nullable DrmInitData drmInitData) { @@ -346,7 +352,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Audio. @@ -425,7 +432,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createAudioSampleFormat( @@ -530,7 +538,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Text. @@ -597,12 +606,13 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } public static Format createTextSampleFormat( @Nullable String id, - String sampleMimeType, + @Nullable String sampleMimeType, @C.SelectionFlags int selectionFlags, @Nullable String language) { return createTextSampleFormat(id, sampleMimeType, selectionFlags, language, null); @@ -610,7 +620,7 @@ public final class Format implements Parcelable { public static Format createTextSampleFormat( @Nullable String id, - String sampleMimeType, + @Nullable String sampleMimeType, @C.SelectionFlags int selectionFlags, @Nullable String language, @Nullable DrmInitData drmInitData) { @@ -681,7 +691,7 @@ public final class Format implements Parcelable { int accessibilityChannel, @Nullable DrmInitData drmInitData, long subsampleOffsetUs, - List initializationData) { + @Nullable List initializationData) { return new Format( id, /* label= */ null, @@ -710,7 +720,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - accessibilityChannel); + accessibilityChannel, + /* exoMediaCryptoType= */ null); } // Image. @@ -752,11 +763,16 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } // Generic. + /** + * @deprecated Use {@link #createContainerFormat(String, String, String, String, String, int, int, + * int, String)} instead. + */ @Deprecated public static Format createContainerFormat( @Nullable String id, @@ -816,7 +832,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, language, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createSampleFormat( @@ -849,7 +866,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } public static Format createSampleFormat( @@ -886,7 +904,8 @@ public final class Format implements Parcelable { /* encoderDelay= */ NO_VALUE, /* encoderPadding= */ NO_VALUE, /* language= */ null, - /* accessibilityChannel= */ NO_VALUE); + /* accessibilityChannel= */ NO_VALUE, + /* exoMediaCryptoType= */ null); } /* package */ Format( @@ -922,7 +941,9 @@ public final class Format implements Parcelable { int encoderPadding, // Audio and text specific. @Nullable String language, - int accessibilityChannel) { + int accessibilityChannel, + // Provided by source. + @Nullable Class exoMediaCryptoType) { this.id = id; this.label = label; this.selectionFlags = selectionFlags; @@ -958,6 +979,8 @@ public final class Format implements Parcelable { // Audio and text specific. this.language = Util.normalizeLanguageCode(language); this.accessibilityChannel = accessibilityChannel; + // Provided by source. + this.exoMediaCryptoType = exoMediaCryptoType; } @SuppressWarnings("ResourceType") @@ -1000,6 +1023,8 @@ public final class Format implements Parcelable { // Audio and text specific. language = in.readString(); accessibilityChannel = in.readInt(); + // Provided by source. + exoMediaCryptoType = null; } public Format copyWithMaxInputSize(int maxInputSize) { @@ -1031,7 +1056,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { @@ -1063,7 +1089,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithLabel(@Nullable String label) { @@ -1095,7 +1122,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithContainerInfo( @@ -1143,7 +1171,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } @SuppressWarnings("ReferenceEquality") @@ -1222,7 +1251,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { @@ -1254,7 +1284,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithFrameRate(float frameRate) { @@ -1286,42 +1317,24 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { - return new Format( - id, - label, - selectionFlags, - roleFlags, - bitrate, - codecs, - metadata, - containerMimeType, - sampleMimeType, - maxInputSize, - initializationData, - drmInitData, - subsampleOffsetUs, - width, - height, - frameRate, - rotationDegrees, - pixelWidthHeightRatio, - projectionData, - stereoMode, - colorInfo, - channelCount, - sampleRate, - pcmEncoding, - encoderDelay, - encoderPadding, - language, - accessibilityChannel); + return copyWithAdjustments(drmInitData, metadata); } public Format copyWithMetadata(@Nullable Metadata metadata) { + return copyWithAdjustments(drmInitData, metadata); + } + + @SuppressWarnings("ReferenceEquality") + public Format copyWithAdjustments( + @Nullable DrmInitData drmInitData, @Nullable Metadata metadata) { + if (drmInitData == this.drmInitData && metadata == this.metadata) { + return this; + } return new Format( id, label, @@ -1350,7 +1363,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithRotationDegrees(int rotationDegrees) { @@ -1382,7 +1396,8 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); } public Format copyWithBitrate(int bitrate) { @@ -1414,7 +1429,75 @@ public final class Format implements Parcelable { encoderDelay, encoderPadding, language, - accessibilityChannel); + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithVideoSize(int width, int height) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); + } + + public Format copyWithExoMediaCryptoType( + @Nullable Class exoMediaCryptoType) { + return new Format( + id, + label, + selectionFlags, + roleFlags, + bitrate, + codecs, + metadata, + containerMimeType, + sampleMimeType, + maxInputSize, + initializationData, + drmInitData, + subsampleOffsetUs, + width, + height, + frameRate, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + channelCount, + sampleRate, + pcmEncoding, + encoderDelay, + encoderPadding, + language, + accessibilityChannel, + exoMediaCryptoType); } /** @@ -1493,6 +1576,8 @@ public final class Format implements Parcelable { // Audio and text specific. result = 31 * result + (language == null ? 0 : language.hashCode()); result = 31 * result + accessibilityChannel; + // Provided by source. + result = 31 * result + (exoMediaCryptoType == null ? 0 : exoMediaCryptoType.hashCode()); hashCode = result; } return hashCode; @@ -1528,6 +1613,7 @@ public final class Format implements Parcelable { && accessibilityChannel == other.accessibilityChannel && Float.compare(frameRate, other.frameRate) == 0 && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 + && Util.areEqual(exoMediaCryptoType, other.exoMediaCryptoType) && Util.areEqual(id, other.id) && Util.areEqual(label, other.label) && Util.areEqual(codecs, other.codecs) diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/FormatHolder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/FormatHolder.java index 5da8d0f9f..7d21182de 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/FormatHolder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/FormatHolder.java @@ -16,24 +16,28 @@ package com.google.android.exoplayer2; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.drm.DecryptionResource; +import com.google.android.exoplayer2.drm.DrmSession; /** * Holds a {@link Format}. */ public final class FormatHolder { - /** - * Whether the object expected to populate {@link #format} is also expected to populate {@link - * #decryptionResource}. - */ + /** Whether the {@link #format} setter also sets the {@link #drmSession} field. */ // TODO: Remove once all Renderers and MediaSources have migrated to the new DRM model [Internal // ref: b/129764794]. - public boolean decryptionResourceIsProvided; + public boolean includesDrmSession; /** An accompanying context for decrypting samples in the format. */ - @Nullable public DecryptionResource decryptionResource; + @Nullable public DrmSession drmSession; /** The held {@link Format}. */ @Nullable public Format format; + + /** Clears the holder. */ + public void clear() { + includesDrmSession = false; + drmSession = null; + format = null; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index a21afc4b5..850d2b7d1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -59,8 +59,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final MediaSource mediaSource; @Nullable private MediaPeriodHolder next; - @Nullable private TrackGroupArray trackGroups; - @Nullable private TrackSelectorResult trackSelectorResult; + private TrackGroupArray trackGroups; + private TrackSelectorResult trackSelectorResult; private long rendererPositionOffsetUs; /** @@ -72,6 +72,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * @param allocator The allocator. * @param mediaSource The media source that produced the media period. * @param info Information used to identify this media period in its timeline period. + * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each + * renderer. */ public MediaPeriodHolder( RendererCapabilities[] rendererCapabilities, @@ -79,13 +81,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - MediaPeriodInfo info) { + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; this.mediaSource = mediaSource; this.uid = info.id.periodUid; this.info = info; + this.trackGroups = TrackGroupArray.EMPTY; + this.trackSelectorResult = emptyTrackSelectorResult; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = @@ -167,8 +172,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void handlePrepared(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { prepared = true; trackGroups = mediaPeriod.getTrackGroups(); - TrackSelectorResult selectorResult = - Assertions.checkNotNull(selectTracks(playbackSpeed, timeline)); + TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline); long newStartPositionUs = applyTrackSelection( selectorResult, info.startPositionUs, /* forceRecreateStreams= */ false); @@ -202,22 +206,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } /** - * Selects tracks for the period and returns the new result if the selection changed. Must only be - * called if {@link #prepared} is {@code true}. + * Selects tracks for the period. Must only be called if {@link #prepared} is {@code true}. + * + *

    The new track selection needs to be applied with {@link + * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect. * * @param playbackSpeed The current playback speed. * @param timeline The current {@link Timeline}. - * @return The {@link TrackSelectorResult} if the result changed. Or null if nothing changed. + * @return The {@link TrackSelectorResult}. * @throws ExoPlaybackException If an error occurs during track selection. */ - @Nullable public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline); - if (selectorResult.isEquivalent(trackSelectorResult)) { - return null; - } for (TrackSelection trackSelection : selectorResult.selections.getAll()) { if (trackSelection != null) { trackSelection.onPlaybackSpeed(playbackSpeed); @@ -303,7 +305,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Releases the media period. No other method should be called after the release. */ public void release() { disableTrackSelectionsInResult(); - trackSelectorResult = null; releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); } @@ -331,25 +332,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return next; } - /** - * Returns the {@link TrackGroupArray} exposed by this media period. Must only be called if {@link - * #prepared} is {@code true}. - */ + /** Returns the {@link TrackGroupArray} exposed by this media period. */ public TrackGroupArray getTrackGroups() { - return Assertions.checkNotNull(trackGroups); + return trackGroups; } - /** - * Returns the {@link TrackSelectorResult} which is currently applied. Must only be called if - * {@link #prepared} is {@code true}. - */ + /** Returns the {@link TrackSelectorResult} which is currently applied. */ public TrackSelectorResult getTrackSelectorResult() { - return Assertions.checkNotNull(trackSelectorResult); + return trackSelectorResult; } private void enableTrackSelectionsInResult() { - TrackSelectorResult trackSelectorResult = this.trackSelectorResult; - if (!isLoadingMediaPeriod() || trackSelectorResult == null) { + if (!isLoadingMediaPeriod()) { return; } for (int i = 0; i < trackSelectorResult.length; i++) { @@ -362,8 +356,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void disableTrackSelectionsInResult() { - TrackSelectorResult trackSelectorResult = this.trackSelectorResult; - if (!isLoadingMediaPeriod() || trackSelectorResult == null) { + if (!isLoadingMediaPeriod()) { return; } for (int i = 0; i < trackSelectorResult.length; i++) { @@ -394,7 +387,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ private void associateNoSampleRenderersWithEmptySampleStream( @NullableType SampleStream[] sampleStreams) { - TrackSelectorResult trackSelectorResult = Assertions.checkNotNull(this.trackSelectorResult); for (int i = 0; i < rendererCapabilities.length; i++) { if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE && trackSelectorResult.isRendererEnabled(i)) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 2927d0311..901b7b4d9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -15,13 +15,14 @@ */ package com.google.android.exoplayer2; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -46,11 +47,11 @@ import com.google.android.exoplayer2.util.Assertions; private Timeline timeline; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private @Nullable MediaPeriodHolder playing; - private @Nullable MediaPeriodHolder reading; - private @Nullable MediaPeriodHolder loading; + @Nullable private MediaPeriodHolder playing; + @Nullable private MediaPeriodHolder reading; + @Nullable private MediaPeriodHolder loading; private int length; - private @Nullable Object oldFrontPeriodUid; + @Nullable private Object oldFrontPeriodUid; private long oldFrontPeriodWindowSequenceNumber; /** Creates a new media period queue. */ @@ -127,21 +128,24 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Enqueues a new media period based on the specified information as the new loading media period, - * and returns it. + * Enqueues a new media period holder based on the specified information as the new loading media + * period, and returns it. * * @param rendererCapabilities The renderer capabilities. * @param trackSelector The track selector. * @param allocator The allocator. * @param mediaSource The media source that produced the media period. * @param info Information used to identify this media period in its timeline period. + * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each + * renderer. */ - public MediaPeriod enqueueNextMediaPeriod( + public MediaPeriodHolder enqueueNextMediaPeriodHolder( RendererCapabilities[] rendererCapabilities, TrackSelector trackSelector, Allocator allocator, MediaSource mediaSource, - MediaPeriodInfo info) { + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { long rendererPositionOffsetUs = loading == null ? (info.id.isAd() && info.contentPositionUs != C.TIME_UNSET @@ -155,54 +159,44 @@ import com.google.android.exoplayer2.util.Assertions; trackSelector, allocator, mediaSource, - info); + info, + emptyTrackSelectorResult); if (loading != null) { - Assertions.checkState(hasPlayingPeriod()); loading.setNext(newPeriodHolder); + } else { + playing = newPeriodHolder; + reading = newPeriodHolder; } oldFrontPeriodUid = null; loading = newPeriodHolder; length++; - return newPeriodHolder.mediaPeriod; + return newPeriodHolder; } /** * Returns the loading period holder which is at the end of the queue, or null if the queue is * empty. */ + @Nullable public MediaPeriodHolder getLoadingPeriod() { return loading; } /** * Returns the playing period holder which is at the front of the queue, or null if the queue is - * empty or hasn't started playing. + * empty. */ + @Nullable public MediaPeriodHolder getPlayingPeriod() { return playing; } - /** - * Returns the reading period holder, or null if the queue is empty or the player hasn't started - * reading. - */ + /** Returns the reading period holder, or null if the queue is empty. */ + @Nullable public MediaPeriodHolder getReadingPeriod() { return reading; } - /** - * Returns the period holder in the front of the queue which is the playing period holder when - * playing, or null if the queue is empty. - */ - public MediaPeriodHolder getFrontPeriod() { - return hasPlayingPeriod() ? playing : loading; - } - - /** Returns whether the reading and playing period holders are set. */ - public boolean hasPlayingPeriod() { - return playing != null; - } - /** * Continues reading from the next period holder in the queue. * @@ -216,28 +210,26 @@ import com.google.android.exoplayer2.util.Assertions; /** * Dequeues the playing period holder from the front of the queue and advances the playing period - * holder to be the next item in the queue. If the playing period holder is unset, set it to the - * item in the front of the queue. + * holder to be the next item in the queue. * * @return The updated playing period holder, or null if the queue is or becomes empty. */ + @Nullable public MediaPeriodHolder advancePlayingPeriod() { - if (playing != null) { - if (playing == reading) { - reading = playing.getNext(); - } - playing.release(); - length--; - if (length == 0) { - loading = null; - oldFrontPeriodUid = playing.uid; - oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber; - } - playing = playing.getNext(); - } else { - playing = loading; - reading = loading; + if (playing == null) { + return null; } + if (playing == reading) { + reading = playing.getNext(); + } + playing.release(); + length--; + if (length == 0) { + loading = null; + oldFrontPeriodUid = playing.uid; + oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber; + } + playing = playing.getNext(); return playing; } @@ -273,12 +265,12 @@ import com.google.android.exoplayer2.util.Assertions; * of queue (typically the playing one) for later reuse. */ public void clear(boolean keepFrontPeriodUid) { - MediaPeriodHolder front = getFrontPeriod(); + MediaPeriodHolder front = playing; if (front != null) { oldFrontPeriodUid = keepFrontPeriodUid ? front.uid : null; oldFrontPeriodWindowSequenceNumber = front.info.id.windowSequenceNumber; - front.release(); removeAfter(front); + front.release(); } else if (!keepFrontPeriodUid) { oldFrontPeriodUid = null; } @@ -305,7 +297,7 @@ import com.google.android.exoplayer2.util.Assertions; // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // handled here. MediaPeriodHolder previousPeriodHolder = null; - MediaPeriodHolder periodHolder = getFrontPeriod(); + MediaPeriodHolder periodHolder = playing; while (periodHolder != null) { MediaPeriodInfo oldPeriodInfo = periodHolder.info; @@ -441,7 +433,7 @@ import com.google.android.exoplayer2.util.Assertions; } } } - MediaPeriodHolder mediaPeriodHolder = getFrontPeriod(); + MediaPeriodHolder mediaPeriodHolder = playing; while (mediaPeriodHolder != null) { if (mediaPeriodHolder.uid.equals(periodUid)) { // Reuse window sequence number of first exact period match. @@ -449,7 +441,7 @@ import com.google.android.exoplayer2.util.Assertions; } mediaPeriodHolder = mediaPeriodHolder.getNext(); } - mediaPeriodHolder = getFrontPeriod(); + mediaPeriodHolder = playing; while (mediaPeriodHolder != null) { int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid); if (indexOfHolderInTimeline != C.INDEX_UNSET) { @@ -462,7 +454,13 @@ import com.google.android.exoplayer2.util.Assertions; mediaPeriodHolder = mediaPeriodHolder.getNext(); } // If no match is found, create new sequence number. - return nextWindowSequenceNumber++; + long windowSequenceNumber = nextWindowSequenceNumber++; + if (playing == null) { + // If the queue is empty, save it as old front uid to allow later reuse. + oldFrontPeriodUid = periodUid; + oldFrontPeriodWindowSequenceNumber = windowSequenceNumber; + } + return windowSequenceNumber; } /** @@ -486,7 +484,7 @@ import com.google.android.exoplayer2.util.Assertions; */ private boolean updateForPlaybackModeChange() { // Find the last existing period holder that matches the new period order. - MediaPeriodHolder lastValidPeriodHolder = getFrontPeriod(); + MediaPeriodHolder lastValidPeriodHolder = playing; if (lastValidPeriodHolder == null) { return true; } @@ -519,7 +517,7 @@ import com.google.android.exoplayer2.util.Assertions; lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); // If renderers may have read from a period that's been removed, it is necessary to restart. - return !readingPeriodRemoved || !hasPlayingPeriod(); + return !readingPeriodRemoved; } /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index e901025a0..b0f690d3e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link Renderer} implementation whose track type is {@link C#TRACK_TYPE_NONE} and does not @@ -27,10 +28,10 @@ import java.io.IOException; */ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities { - private RendererConfiguration configuration; + @MonotonicNonNull private RendererConfiguration configuration; private int index; private int state; - private SampleStream stream; + @Nullable private SampleStream stream; private boolean streamIsFinal; @Override @@ -49,6 +50,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities } @Override + @Nullable public MediaClock getMediaClock() { return null; } @@ -113,6 +115,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities } @Override + @Nullable public final SampleStream getStream() { return stream; } @@ -182,11 +185,13 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities // RendererCapabilities implementation. @Override + @Capabilities public int supportsFormat(Format format) throws ExoPlaybackException { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override + @AdaptiveSupport public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SUPPORTED; } @@ -283,8 +288,10 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities // Methods to be called by subclasses. /** - * Returns the configuration set when the renderer was most recently enabled. + * Returns the configuration set when the renderer was most recently enabled, or {@code null} if + * the renderer has never been enabled. */ + @Nullable protected final RendererConfiguration getConfiguration() { return configuration; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 7107963c8..9d2a3b545 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -35,8 +35,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** The current {@link Timeline}. */ public final Timeline timeline; - /** The current manifest. */ - public final @Nullable Object manifest; /** The {@link MediaPeriodId} of the currently playing media period in the {@link #timeline}. */ public final MediaPeriodId periodId; /** @@ -53,7 +51,9 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; */ public final long contentPositionUs; /** The current playback state. One of the {@link Player}.STATE_ constants. */ - public final int playbackState; + @Player.State public final int playbackState; + /** The current playback error, or null if this is not an error state. */ + @Nullable public final ExoPlaybackException playbackError; /** Whether the player is currently loading. */ public final boolean isLoading; /** The currently available track groups. */ @@ -92,11 +92,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long startPositionUs, TrackSelectorResult emptyTrackSelectorResult) { return new PlaybackInfo( Timeline.EMPTY, - /* manifest= */ null, DUMMY_MEDIA_PERIOD_ID, startPositionUs, /* contentPositionUs= */ C.TIME_UNSET, Player.STATE_IDLE, + /* playbackError= */ null, /* isLoading= */ false, TrackGroupArray.EMPTY, emptyTrackSelectorResult, @@ -110,7 +110,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * Create playback info. * * @param timeline See {@link #timeline}. - * @param manifest See {@link #manifest}. * @param periodId See {@link #periodId}. * @param startPositionUs See {@link #startPositionUs}. * @param contentPositionUs See {@link #contentPositionUs}. @@ -125,11 +124,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; */ public PlaybackInfo( Timeline timeline, - @Nullable Object manifest, MediaPeriodId periodId, long startPositionUs, long contentPositionUs, - int playbackState, + @Player.State int playbackState, + @Nullable ExoPlaybackException playbackError, boolean isLoading, TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult, @@ -138,11 +137,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long totalBufferedDurationUs, long positionUs) { this.timeline = timeline; - this.manifest = manifest; this.periodId = periodId; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; this.playbackState = playbackState; + this.playbackError = playbackError; this.isLoading = isLoading; this.trackGroups = trackGroups; this.trackSelectorResult = trackSelectorResult; @@ -157,49 +156,30 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; * * @param shuffleModeEnabled Whether shuffle mode is enabled. * @param window A writable {@link Timeline.Window}. + * @param period A writable {@link Timeline.Period}. * @return A dummy media period id for the first-to-be-played period of the current timeline. */ public MediaPeriodId getDummyFirstMediaPeriodId( - boolean shuffleModeEnabled, Timeline.Window window) { + boolean shuffleModeEnabled, Timeline.Window window, Timeline.Period period) { if (timeline.isEmpty()) { return DUMMY_MEDIA_PERIOD_ID; } - int firstPeriodIndex = - timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) - .firstPeriodIndex; - return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex)); + int firstWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled); + int firstPeriodIndex = timeline.getWindow(firstWindowIndex, window).firstPeriodIndex; + int currentPeriodIndex = timeline.getIndexOfPeriod(periodId.periodUid); + long windowSequenceNumber = C.INDEX_UNSET; + if (currentPeriodIndex != C.INDEX_UNSET) { + int currentWindowIndex = timeline.getPeriod(currentPeriodIndex, period).windowIndex; + if (firstWindowIndex == currentWindowIndex) { + // Keep window sequence number if the new position is still in the same window. + windowSequenceNumber = periodId.windowSequenceNumber; + } + } + return new MediaPeriodId(timeline.getUidOfPeriod(firstPeriodIndex), windowSequenceNumber); } /** - * Copies playback info and resets playing and loading position. - * - * @param periodId New playing and loading {@link MediaPeriodId}. - * @param startPositionUs New start position. See {@link #startPositionUs}. - * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored - * if {@code periodId.isAd()} is true. - * @return Copied playback info with reset position. - */ - @CheckResult - public PlaybackInfo resetToNewPosition( - MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { - return new PlaybackInfo( - timeline, - manifest, - periodId, - startPositionUs, - periodId.isAd() ? contentPositionUs : C.TIME_UNSET, - playbackState, - isLoading, - trackGroups, - trackSelectorResult, - periodId, - startPositionUs, - /* totalBufferedDurationUs= */ 0, - startPositionUs); - } - - /** - * Copied playback info with new playing position. + * Copies playback info with new playing position. * * @param periodId New playing media period. See {@link #periodId}. * @param positionUs New position. See {@link #positionUs}. @@ -216,11 +196,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; long totalBufferedDurationUs) { return new PlaybackInfo( timeline, - manifest, periodId, positionUs, periodId.isAd() ? contentPositionUs : C.TIME_UNSET, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -231,21 +211,20 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; } /** - * Copies playback info with new timeline and manifest. + * Copies playback info with the new timeline. * * @param timeline New timeline. See {@link #timeline}. - * @param manifest New manifest. See {@link #manifest}. - * @return Copied playback info with new timeline and manifest. + * @return Copied playback info with the new timeline. */ @CheckResult - public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { + public PlaybackInfo copyWithTimeline(Timeline timeline) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -265,11 +244,35 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithPlaybackState(int playbackState) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, playbackState, + playbackError, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with a playback error. + * + * @param playbackError The error. See {@link #playbackError}. + * @return Copied playback info with the playback error. + */ + @CheckResult + public PlaybackInfo copyWithPlaybackError(@Nullable ExoPlaybackException playbackError) { + return new PlaybackInfo( + timeline, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -289,11 +292,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithIsLoading(boolean isLoading) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -315,11 +318,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, @@ -339,11 +342,11 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) { return new PlaybackInfo( timeline, - manifest, periodId, startPositionUs, contentPositionUs, playbackState, + playbackError, isLoading, trackGroups, trackSelectorResult, diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/Player.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/Player.java index 9fa0188b7..3871730c9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/Player.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/Player.java @@ -16,12 +16,12 @@ package com.google.android.exoplayer2; import android.os.Looper; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C.VideoScalingMode; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioListener; @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.spherical.CameraMotionListener; @@ -216,7 +217,7 @@ public interface Player { * * @param surface The surface to clear. */ - void clearVideoSurface(Surface surface); + void clearVideoSurface(@Nullable Surface surface); /** * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for @@ -239,7 +240,7 @@ public interface Player { * * @param surfaceHolder The surface holder. */ - void setVideoSurfaceHolder(SurfaceHolder surfaceHolder); + void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); /** * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being @@ -247,7 +248,7 @@ public interface Player { * * @param surfaceHolder The surface holder to clear. */ - void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder); + void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder); /** * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the @@ -255,7 +256,7 @@ public interface Player { * * @param surfaceView The surface view. */ - void setVideoSurfaceView(SurfaceView surfaceView); + void setVideoSurfaceView(@Nullable SurfaceView surfaceView); /** * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one @@ -263,7 +264,7 @@ public interface Player { * * @param surfaceView The texture view to clear. */ - void clearVideoSurfaceView(SurfaceView surfaceView); + void clearVideoSurfaceView(@Nullable SurfaceView surfaceView); /** * Sets the {@link TextureView} onto which video will be rendered. The player will track the @@ -271,7 +272,7 @@ public interface Player { * * @param textureView The texture view. */ - void setVideoTextureView(TextureView textureView); + void setVideoTextureView(@Nullable TextureView textureView); /** * Clears the {@link TextureView} onto which video is being rendered if it matches the one @@ -279,7 +280,31 @@ public interface Player { * * @param textureView The texture view to clear. */ - void clearVideoTextureView(TextureView textureView); + void clearVideoTextureView(@Nullable TextureView textureView); + + /** + * Sets the video decoder output buffer renderer. This is intended for use only with extension + * renderers that accept {@link C#MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER}. For most use + * cases, an output surface or view should be passed via {@link #setVideoSurface(Surface)} or + * {@link #setVideoSurfaceView(SurfaceView)} instead. + * + * @param videoDecoderOutputBufferRenderer The video decoder output buffer renderer, or {@code + * null} to clear the output buffer renderer. + */ + void setVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer); + + /** Clears the video decoder output buffer renderer. */ + void clearVideoDecoderOutputBufferRenderer(); + + /** + * Clears the video decoder output buffer renderer if it matches the one passed. Else does + * nothing. + * + * @param videoDecoderOutputBufferRenderer The video decoder output buffer renderer to clear. + */ + void clearVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer); } /** The text component of a {@link Player}. */ @@ -324,6 +349,29 @@ public interface Player { */ interface EventListener { + /** + * Called when the timeline has been refreshed. + * + *

    Note that if the timeline has changed then a position discontinuity may also have + * occurred. For example, the current period index may have changed as a result of periods being + * added or removed from the timeline. This will not be reported via a separate call to + * {@link #onPositionDiscontinuity(int)}. + * + * @param timeline The latest timeline. Never null, but may be empty. + * @param reason The {@link TimelineChangeReason} responsible for this timeline change. + */ + @SuppressWarnings("deprecation") + default void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + Object manifest = null; + if (timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = timeline.getWindow(0, window).manifest; + } + // Call deprecated version. + onTimelineChanged(timeline, manifest, reason); + } + /** * Called when the timeline and/or manifest has been refreshed. * @@ -335,7 +383,11 @@ public interface Player { * @param timeline The latest timeline. Never null, but may be empty. * @param manifest The latest manifest. May be null. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. + * @deprecated Use {@link #onTimelineChanged(Timeline, int)} instead. The manifest can be + * accessed by using {@link #getCurrentManifest()} or {@code timeline.getWindow(windowIndex, + * window).manifest} for a given window index. */ + @Deprecated default void onTimelineChanged( Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) {} @@ -361,9 +413,9 @@ public interface Player { * #getPlaybackState()} changes. * * @param playWhenReady Whether playback will proceed when ready. - * @param playbackState One of the {@code STATE} constants. + * @param playbackState The new {@link State playback state}. */ - default void onPlayerStateChanged(boolean playWhenReady, int playbackState) {} + default void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {} /** * Called when the value returned from {@link #getPlaybackSuppressionReason()} changes. @@ -411,8 +463,7 @@ public interface Player { * when the source introduces a discontinuity internally). * *

    When a position discontinuity occurs as a result of a change to the timeline this method - * is not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this - * case. + * is not called. {@link #onTimelineChanged(Timeline, int)} is called in this case. * * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. */ @@ -443,6 +494,18 @@ public interface Player { @Deprecated abstract class DefaultEventListener implements EventListener { + @Override + public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reason) { + Object manifest = null; + if (timeline.getWindowCount() == 1) { + // Legacy behavior was to report the manifest for single window timelines only. + Timeline.Window window = new Timeline.Window(); + manifest = timeline.getWindow(0, window).manifest; + } + // Call deprecated version. + onTimelineChanged(timeline, manifest, reason); + } + @Override @SuppressWarnings("deprecation") public void onTimelineChanged( @@ -451,13 +514,21 @@ public interface Player { onTimelineChanged(timeline, manifest); } - /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, Object, int)} instead. */ + /** @deprecated Use {@link EventListener#onTimelineChanged(Timeline, int)} instead. */ @Deprecated public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { // Do nothing. } } + /** + * Playback state. One of {@link #STATE_IDLE}, {@link #STATE_BUFFERING}, {@link #STATE_READY} or + * {@link #STATE_ENDED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED}) + @interface State {} /** * The player does not have any media to play. */ @@ -548,8 +619,8 @@ public interface Player { int DISCONTINUITY_REASON_INTERNAL = 4; /** - * Reasons for timeline and/or manifest changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, - * {@link #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. + * Reasons for timeline changes. One of {@link #TIMELINE_CHANGE_REASON_PREPARED}, {@link + * #TIMELINE_CHANGE_REASON_RESET} or {@link #TIMELINE_CHANGE_REASON_DYNAMIC}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -559,13 +630,9 @@ public interface Player { TIMELINE_CHANGE_REASON_DYNAMIC }) @interface TimelineChangeReason {} - /** - * Timeline and manifest changed as a result of a player initialization with new media. - */ + /** Timeline and manifest changed as a result of a player initialization with new media. */ int TIMELINE_CHANGE_REASON_PREPARED = 0; - /** - * Timeline and manifest changed as a result of a player reset. - */ + /** Timeline and manifest changed as a result of a player reset. */ int TIMELINE_CHANGE_REASON_RESET = 1; /** * Timeline or manifest changed as a result of an dynamic update introduced by the played media. @@ -613,10 +680,11 @@ public interface Player { void removeListener(EventListener listener); /** - * Returns the current state of the player. + * Returns the current {@link State playback state} of the player. * - * @return One of the {@code STATE} constants defined in this interface. + * @return The current {@link State playback state}. */ + @State int getPlaybackState(); /** @@ -677,7 +745,7 @@ public interface Player { /** * Sets the {@link RepeatMode} to be used for playback. * - * @param repeatMode A repeat mode. + * @param repeatMode The repeat mode. */ void setRepeatMode(@RepeatMode int repeatMode); @@ -772,13 +840,10 @@ public interface Player { /** * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment. - *

    - * Playback parameters changes may cause the player to buffer. - * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever - * the currently active playback parameters change. When that listener is called, the parameters - * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch - * may be out of range, in which case they are constrained to a set of permitted values. If it is - * not possible to change the playback parameters, the listener will not be invoked. + * + *

    Playback parameters changes may cause the player to buffer. {@link + * EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever the + * currently active playback parameters change. * * @param playbackParameters The playback parameters, or {@code null} to use the defaults. */ @@ -920,6 +985,13 @@ public interface Player { */ boolean isCurrentWindowDynamic(); + /** + * Returns whether the current window is live, or {@code false} if the {@link Timeline} is empty. + * + * @see Timeline.Window#isLive + */ + boolean isCurrentWindowLive(); + /** * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is * empty. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 7904942c1..49309181a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -55,7 +55,7 @@ public final class PlayerMessage { private final Timeline timeline; private int type; - private @Nullable Object payload; + @Nullable private Object payload; private Handler handler; private int windowIndex; private long positionMs; @@ -134,7 +134,8 @@ public final class PlayerMessage { } /** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */ - public @Nullable Object getPayload() { + @Nullable + public Object getPayload() { return payload; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/Renderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/Renderer.java index 9f52e8d9d..b699162e2 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; @@ -31,7 +32,8 @@ import java.lang.annotation.RetentionPolicy; * valid state transitions are shown below, annotated with the methods that are called during each * transition. * - *

    Renderer state transitions + *

    Renderer state
+ * transitions */ public interface Renderer extends PlayerMessage.Target { @@ -87,11 +89,12 @@ public interface Renderer extends PlayerMessage.Target { /** * 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. + * 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. */ + @Nullable MediaClock getMediaClock(); /** @@ -147,9 +150,8 @@ public interface Renderer extends PlayerMessage.Target { void replaceStream(Format[] formats, SampleStream stream, long offsetUs) throws ExoPlaybackException; - /** - * Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. - */ + /** Returns the {@link SampleStream} being consumed, or null if the renderer is disabled. */ + @Nullable SampleStream getStream(); /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index de0d48138..a75765262 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -15,7 +15,12 @@ */ package com.google.android.exoplayer2; +import android.annotation.SuppressLint; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.MimeTypes; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Defines the capabilities of a {@link Renderer}. @@ -23,10 +28,22 @@ import com.google.android.exoplayer2.util.MimeTypes; public interface RendererCapabilities { /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + * Level of renderer support for a format. One of {@link #FORMAT_HANDLED}, {@link + * #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, {@link + * #FORMAT_UNSUPPORTED_SUBTYPE} or {@link #FORMAT_UNSUPPORTED_TYPE}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + FORMAT_HANDLED, + FORMAT_EXCEEDS_CAPABILITIES, + FORMAT_UNSUPPORTED_DRM, + FORMAT_UNSUPPORTED_SUBTYPE, + FORMAT_UNSUPPORTED_TYPE + }) + @interface FormatSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link FormatSupport} only. */ int FORMAT_SUPPORT_MASK = 0b111; /** * The {@link Renderer} is capable of rendering the format. @@ -72,9 +89,15 @@ public interface RendererCapabilities { int FORMAT_UNSUPPORTED_TYPE = 0b000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. + * Level of renderer support for adaptive format switches. One of {@link #ADAPTIVE_SEAMLESS}, + * {@link #ADAPTIVE_NOT_SEAMLESS} or {@link #ADAPTIVE_NOT_SUPPORTED}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ADAPTIVE_SEAMLESS, ADAPTIVE_NOT_SEAMLESS, ADAPTIVE_NOT_SUPPORTED}) + @interface AdaptiveSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link AdaptiveSupport} only. */ int ADAPTIVE_SUPPORT_MASK = 0b11000; /** * The {@link Renderer} can seamlessly adapt between formats. @@ -91,9 +114,15 @@ public interface RendererCapabilities { int ADAPTIVE_NOT_SUPPORTED = 0b00000; /** - * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + * Level of renderer support for tunneling. One of {@link #TUNNELING_SUPPORTED} or {@link + * #TUNNELING_NOT_SUPPORTED}. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({TUNNELING_SUPPORTED, TUNNELING_NOT_SUPPORTED}) + @interface TunnelingSupport {} + + /** A mask to apply to {@link Capabilities} to obtain the {@link TunnelingSupport} only. */ int TUNNELING_SUPPORT_MASK = 0b100000; /** * The {@link Renderer} supports tunneled output. @@ -104,6 +133,133 @@ public interface RendererCapabilities { */ int TUNNELING_NOT_SUPPORTED = 0b000000; + /** + * Combined renderer capabilities. + * + *

    This is a bitwise OR of {@link FormatSupport}, {@link AdaptiveSupport} and {@link + * TunnelingSupport}. Use {@link #getFormatSupport(int)}, {@link #getAdaptiveSupport(int)} or + * {@link #getTunnelingSupport(int)} to obtain the individual flags. And use {@link #create(int)} + * or {@link #create(int, int, int)} to create the combined capabilities. + * + *

    Possible values: + * + *

      + *
    • {@link FormatSupport}: The level of support for the format itself. One of {@link + * #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, + * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. + *
    • {@link AdaptiveSupport}: The level of support for adapting from the format to another + * format of the same mime type. One of {@link #ADAPTIVE_SEAMLESS}, {@link + * #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of + * support for the format itself is {@link #FORMAT_HANDLED} or {@link + * #FORMAT_EXCEEDS_CAPABILITIES}. + *
    • {@link TunnelingSupport}: The level of support for tunneling. One of {@link + * #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of + * support for the format itself is {@link #FORMAT_HANDLED} or {@link + * #FORMAT_EXCEEDS_CAPABILITIES}. + *
    + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + // Intentionally empty to prevent assignment or comparison with individual flags without masking. + @IntDef({}) + @interface Capabilities {} + + /** + * Returns {@link Capabilities} for the given {@link FormatSupport}. + * + *

    The {@link AdaptiveSupport} is set to {@link #ADAPTIVE_NOT_SUPPORTED} and {{@link + * TunnelingSupport} is set to {@link #TUNNELING_NOT_SUPPORTED}. + * + * @param formatSupport The {@link FormatSupport}. + * @return The combined {@link Capabilities} of the given {@link FormatSupport}, {@link + * #ADAPTIVE_NOT_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}. + */ + @Capabilities + static int create(@FormatSupport int formatSupport) { + return create(formatSupport, ADAPTIVE_NOT_SUPPORTED, TUNNELING_NOT_SUPPORTED); + } + + /** + * Returns {@link Capabilities} combining the given {@link FormatSupport}, {@link AdaptiveSupport} + * and {@link TunnelingSupport}. + * + * @param formatSupport The {@link FormatSupport}. + * @param adaptiveSupport The {@link AdaptiveSupport}. + * @param tunnelingSupport The {@link TunnelingSupport}. + * @return The combined {@link Capabilities}. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @Capabilities + static int create( + @FormatSupport int formatSupport, + @AdaptiveSupport int adaptiveSupport, + @TunnelingSupport int tunnelingSupport) { + return formatSupport | adaptiveSupport | tunnelingSupport; + } + + /** + * Returns the {@link FormatSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link FormatSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @FormatSupport + static int getFormatSupport(@Capabilities int supportFlags) { + return supportFlags & FORMAT_SUPPORT_MASK; + } + + /** + * Returns the {@link AdaptiveSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link AdaptiveSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @AdaptiveSupport + static int getAdaptiveSupport(@Capabilities int supportFlags) { + return supportFlags & ADAPTIVE_SUPPORT_MASK; + } + + /** + * Returns the {@link TunnelingSupport} from the combined {@link Capabilities}. + * + * @param supportFlags The combined {@link Capabilities}. + * @return The {@link TunnelingSupport} only. + */ + // Suppression needed for IntDef casting. + @SuppressLint("WrongConstant") + @TunnelingSupport + static int getTunnelingSupport(@Capabilities int supportFlags) { + return supportFlags & TUNNELING_SUPPORT_MASK; + } + + /** + * Returns string representation of a {@link FormatSupport} flag. + * + * @param formatSupport A {@link FormatSupport} flag. + * @return A string representation of the flag. + */ + static String getFormatSupportString(@FormatSupport int formatSupport) { + switch (formatSupport) { + case RendererCapabilities.FORMAT_HANDLED: + return "YES"; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + return "NO_EXCEEDS_CAPABILITIES"; + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + return "NO_UNSUPPORTED_DRM"; + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + return "NO_UNSUPPORTED_TYPE"; + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + return "NO"; + default: + throw new IllegalStateException(); + } + } + /** * Returns the track type that the {@link Renderer} handles. For example, a video renderer will * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a @@ -115,39 +271,23 @@ public interface RendererCapabilities { int getTrackType(); /** - * Returns the extent to which the {@link Renderer} supports a given format. The returned value is - * the bitwise OR of three properties: - *

      - *
    • The level of support for the format itself. One of {@link #FORMAT_HANDLED}, - * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, - * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
    • - *
    • The level of support for adapting from the format to another format of the same mime type. - * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of support for the format itself is - * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.
    • - *
    • The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and - * {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of support for the format itself is - * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}.
    • - *
    - * The individual properties can be retrieved by performing a bitwise AND with - * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and - * {@link #TUNNELING_SUPPORT_MASK} respectively. + * Returns the extent to which the {@link Renderer} supports a given format. * * @param format The format. - * @return The extent to which the renderer is capable of supporting the given format. + * @return The {@link Capabilities} for this format. * @throws ExoPlaybackException If an error occurs. */ + @Capabilities int supportsFormat(Format format) throws ExoPlaybackException; /** * Returns the extent to which the {@link Renderer} supports adapting between supported formats - * that have different mime types. + * that have different MIME types. * - * @return The extent to which the renderer supports adapting between supported formats that have - * different mime types. One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport} for adapting between supported formats that have different + * MIME types. * @throws ExoPlaybackException If an error occurs. */ + @AdaptiveSupport int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException; - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index fa48a79be..d7795d068 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -23,15 +23,15 @@ import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; import android.os.Looper; -import androidx.annotation.Nullable; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; -import com.google.android.exoplayer2.audio.AudioFocusManager; import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AuxEffectInfo; @@ -45,14 +45,17 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.PriorityTaskManager; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.spherical.CameraMotionListener; @@ -66,7 +69,7 @@ import java.util.concurrent.CopyOnWriteArraySet; /** * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can - * be obtained from {@link ExoPlayerFactory}. + * be obtained from {@link SimpleExoPlayer.Builder}. */ public class SimpleExoPlayer extends BasePlayer implements ExoPlayer, @@ -79,6 +82,232 @@ public class SimpleExoPlayer extends BasePlayer @Deprecated public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {} + /** + * A builder for {@link SimpleExoPlayer} instances. + * + *

    See {@link #Builder(Context)} for the list of default values. + */ + public static final class Builder { + + private final Context context; + private final RenderersFactory renderersFactory; + + private Clock clock; + private TrackSelector trackSelector; + private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; + private AnalyticsCollector analyticsCollector; + private Looper looper; + private boolean useLazyPreparation; + private boolean buildCalled; + + /** + * Creates a builder. + * + *

    Use {@link #Builder(Context, RenderersFactory)} instead, if you intend to provide a custom + * {@link RenderersFactory}. This is to ensure that ProGuard or R8 can remove ExoPlayer's {@link + * DefaultRenderersFactory} from the APK. + * + *

    The builder uses the following default values: + * + *

      + *
    • {@link RenderersFactory}: {@link DefaultRenderersFactory} + *
    • {@link TrackSelector}: {@link DefaultTrackSelector} + *
    • {@link LoadControl}: {@link DefaultLoadControl} + *
    • {@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} + *
    • {@link Looper}: The {@link Looper} associated with the current thread, or the {@link + * Looper} of the application's main thread if the current thread doesn't have a {@link + * Looper} + *
    • {@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT} + *
    • {@code useLazyPreparation}: {@code true} + *
    • {@link Clock}: {@link Clock#DEFAULT} + *
    + * + * @param context A {@link Context}. + */ + public Builder(Context context) { + this(context, new DefaultRenderersFactory(context)); + } + + /** + * Creates a builder with a custom {@link RenderersFactory}. + * + *

    See {@link #Builder(Context)} for a list of default values. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. + */ + public Builder(Context context, RenderersFactory renderersFactory) { + this( + context, + renderersFactory, + new DefaultTrackSelector(context), + new DefaultLoadControl(), + DefaultBandwidthMeter.getSingletonInstance(context), + Util.getLooper(), + new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, + Clock.DEFAULT); + } + + /** + * Creates a builder with the specified custom components. + * + *

    Note that this constructor is only useful if you try to ensure that ExoPlayer's default + * components can be removed by ProGuard or R8. For most components except renderers, there is + * only a marginal benefit of doing that. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. + * @param trackSelector A {@link TrackSelector}. + * @param loadControl A {@link LoadControl}. + * @param bandwidthMeter A {@link BandwidthMeter}. + * @param looper A {@link Looper} that must be used for all calls to the player. + * @param analyticsCollector An {@link AnalyticsCollector}. + * @param useLazyPreparation Whether media sources should be initialized lazily. + * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. + */ + public Builder( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Looper looper, + AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, + Clock clock) { + this.context = context; + this.renderersFactory = renderersFactory; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; + this.looper = looper; + this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; + this.clock = clock; + } + + /** + * Sets the {@link TrackSelector} that will be used by the player. + * + * @param trackSelector A {@link TrackSelector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setTrackSelector(TrackSelector trackSelector) { + Assertions.checkState(!buildCalled); + this.trackSelector = trackSelector; + return this; + } + + /** + * Sets the {@link LoadControl} that will be used by the player. + * + * @param loadControl A {@link LoadControl}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLoadControl(LoadControl loadControl) { + Assertions.checkState(!buildCalled); + this.loadControl = loadControl; + return this; + } + + /** + * Sets the {@link BandwidthMeter} that will be used by the player. + * + * @param bandwidthMeter A {@link BandwidthMeter}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { + Assertions.checkState(!buildCalled); + this.bandwidthMeter = bandwidthMeter; + return this; + } + + /** + * Sets the {@link Looper} that must be used for all calls to the player and that is used to + * call listeners on. + * + * @param looper A {@link Looper}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLooper(Looper looper) { + Assertions.checkState(!buildCalled); + this.looper = looper; + return this; + } + + /** + * Sets the {@link AnalyticsCollector} that will collect and forward all player events. + * + * @param analyticsCollector An {@link AnalyticsCollector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) { + Assertions.checkState(!buildCalled); + this.analyticsCollector = analyticsCollector; + return this; + } + + /** + * Sets whether media sources should be initialized lazily. + * + *

    If false, all initial preparation steps (e.g., manifest loads) happen immediately. If + * true, these initial preparations are triggered only when the player starts buffering the + * media. + * + * @param useLazyPreparation Whether to use lazy preparation. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setUseLazyPreparation(boolean useLazyPreparation) { + Assertions.checkState(!buildCalled); + this.useLazyPreparation = useLazyPreparation; + return this; + } + + /** + * Sets the {@link Clock} that will be used by the player. Should only be set for testing + * purposes. + * + * @param clock A {@link Clock}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @VisibleForTesting + public Builder setClock(Clock clock) { + Assertions.checkState(!buildCalled); + this.clock = clock; + return this; + } + + /** + * Builds a {@link SimpleExoPlayer} instance. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public SimpleExoPlayer build() { + Assertions.checkState(!buildCalled); + buildCalled = true; + return new SimpleExoPlayer( + context, + renderersFactory, + trackSelector, + loadControl, + bandwidthMeter, + analyticsCollector, + clock, + looper); + } + } + private static final String TAG = "SimpleExoPlayer"; protected final Renderer[] renderers; @@ -96,13 +325,17 @@ public class SimpleExoPlayer extends BasePlayer private final BandwidthMeter bandwidthMeter; private final AnalyticsCollector analyticsCollector; + private final AudioBecomingNoisyManager audioBecomingNoisyManager; private final AudioFocusManager audioFocusManager; + private final WakeLockManager wakeLockManager; + private final WifiLockManager wifiLockManager; private boolean needSetSurface = true; @Nullable private Format videoFormat; @Nullable private Format audioFormat; + @Nullable private VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer; @Nullable private Surface surface; private boolean ownsSurface; private @C.VideoScalingMode int videoScalingMode; @@ -122,6 +355,7 @@ public class SimpleExoPlayer extends BasePlayer private boolean hasNotifiedFullWrongThreadWarning; @Nullable private PriorityTaskManager priorityTaskManager; private boolean isPriorityTaskManagerRegistered; + private boolean playerReleased; /** * @param context A {@link Context}. @@ -129,79 +363,54 @@ public class SimpleExoPlayer extends BasePlayer * @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance. * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - */ - protected SimpleExoPlayer( - Context context, - RenderersFactory renderersFactory, - TrackSelector trackSelector, - LoadControl loadControl, - BandwidthMeter bandwidthMeter, - @Nullable DrmSessionManager drmSessionManager, - Looper looper) { - this( - context, - renderersFactory, - trackSelector, - loadControl, - drmSessionManager, - bandwidthMeter, - new AnalyticsCollector.Factory(), - looper); - } - - /** - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. - * @param looper The {@link Looper} which must be used for all calls to the player and which is - * used to call listeners on. - */ - protected SimpleExoPlayer( - Context context, - RenderersFactory renderersFactory, - TrackSelector trackSelector, - LoadControl loadControl, - @Nullable DrmSessionManager drmSessionManager, - BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, - Looper looper) { - this( - context, - renderersFactory, - trackSelector, - loadControl, - drmSessionManager, - bandwidthMeter, - analyticsCollectorFactory, - Clock.DEFAULT, - looper); - } - - /** - * @param context A {@link Context}. - * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. - * @param trackSelector The {@link TrackSelector} that will be used by the instance. - * @param loadControl The {@link LoadControl} that will be used by the instance. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance - * will not be used for DRM protected playbacks. - * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. - * @param analyticsCollectorFactory A factory for creating the {@link AnalyticsCollector} that - * will collect and forward all player events. + * @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will + * collect and forward all player events. * @param clock The {@link Clock} that will be used by the instance. Should always be {@link * Clock#DEFAULT}, unless the player is being used from a test. * @param looper The {@link Looper} which must be used for all calls to the player and which is * used to call listeners on. */ + @SuppressWarnings("deprecation") + protected SimpleExoPlayer( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + AnalyticsCollector analyticsCollector, + Clock clock, + Looper looper) { + this( + context, + renderersFactory, + trackSelector, + loadControl, + DrmSessionManager.getDummyDrmSessionManager(), + bandwidthMeter, + analyticsCollector, + clock, + looper); + } + + /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. + * @param clock The {@link Clock} that will be used by the instance. Should always be {@link + * Clock#DEFAULT}, unless the player is being used from a test. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, + * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link + * DrmSessionManager} to the {@link MediaSource} factories. + */ + @Deprecated protected SimpleExoPlayer( Context context, RenderersFactory renderersFactory, @@ -209,10 +418,11 @@ public class SimpleExoPlayer extends BasePlayer LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, BandwidthMeter bandwidthMeter, - AnalyticsCollector.Factory analyticsCollectorFactory, + AnalyticsCollector analyticsCollector, Clock clock, Looper looper) { this.bandwidthMeter = bandwidthMeter; + this.analyticsCollector = analyticsCollector; componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); audioListeners = new CopyOnWriteArraySet<>(); @@ -240,9 +450,9 @@ public class SimpleExoPlayer extends BasePlayer // Build the player and associated objects. player = new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); - analyticsCollector = analyticsCollectorFactory.createAnalyticsCollector(player, clock); - addListener(analyticsCollector); - addListener(componentListener); + analyticsCollector.setPlayer(player); + player.addListener(analyticsCollector); + player.addListener(componentListener); videoDebugListeners.add(analyticsCollector); videoListeners.add(analyticsCollector); audioDebugListeners.add(analyticsCollector); @@ -252,7 +462,11 @@ public class SimpleExoPlayer extends BasePlayer if (drmSessionManager instanceof DefaultDrmSessionManager) { ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); } - audioFocusManager = new AudioFocusManager(context, componentListener); + audioBecomingNoisyManager = + new AudioBecomingNoisyManager(context, eventHandler, componentListener); + audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); + wakeLockManager = new WakeLockManager(context); + wifiLockManager = new WifiLockManager(context); } @Override @@ -310,14 +524,16 @@ public class SimpleExoPlayer extends BasePlayer @Override public void clearVideoSurface() { verifyApplicationThread(); - setVideoSurface(null); + removeSurfaceCallbacks(); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @Override - public void clearVideoSurface(Surface surface) { + public void clearVideoSurface(@Nullable Surface surface) { verifyApplicationThread(); if (surface != null && surface == this.surface) { - setVideoSurface(null); + clearVideoSurface(); } } @@ -325,18 +541,24 @@ public class SimpleExoPlayer extends BasePlayer public void setVideoSurface(@Nullable Surface surface) { verifyApplicationThread(); removeSurfaceCallbacks(); - setVideoSurfaceInternal(surface, false); + if (surface != null) { + clearVideoDecoderOutputBufferRenderer(); + } + setVideoSurfaceInternal(surface, /* ownsSurface= */ false); int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET; maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize); } @Override - public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThread(); removeSurfaceCallbacks(); + if (surfaceHolder != null) { + clearVideoDecoderOutputBufferRenderer(); + } this.surfaceHolder = surfaceHolder; if (surfaceHolder == null) { - setVideoSurfaceInternal(null, false); + setVideoSurfaceInternal(null, /* ownsSurface= */ false); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { surfaceHolder.addCallback(componentListener); @@ -353,7 +575,7 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { verifyApplicationThread(); if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) { setVideoSurfaceHolder(null); @@ -361,34 +583,37 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void setVideoSurfaceView(SurfaceView surfaceView) { + public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } @Override - public void clearVideoSurfaceView(SurfaceView surfaceView) { + public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } @Override - public void setVideoTextureView(TextureView textureView) { + public void setVideoTextureView(@Nullable TextureView textureView) { if (this.textureView == textureView) { return; } verifyApplicationThread(); removeSurfaceCallbacks(); + if (textureView != null) { + clearVideoDecoderOutputBufferRenderer(); + } this.textureView = textureView; needSetSurface = true; if (textureView == null) { - setVideoSurfaceInternal(null, true); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } else { if (textureView.getSurfaceTextureListener() != null) { Log.w(TAG, "Replacing existing SurfaceTextureListener."); } textureView.setSurfaceTextureListener(componentListener); - SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture() - : null; + SurfaceTexture surfaceTexture = + textureView.isAvailable() ? textureView.getSurfaceTexture() : null; if (surfaceTexture == null) { setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); @@ -400,13 +625,39 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void clearVideoTextureView(TextureView textureView) { + public void clearVideoTextureView(@Nullable TextureView textureView) { verifyApplicationThread(); if (textureView != null && textureView == this.textureView) { setVideoTextureView(null); } } + @Override + public void setVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + verifyApplicationThread(); + if (videoDecoderOutputBufferRenderer != null) { + clearVideoSurface(); + } + setVideoDecoderOutputBufferRendererInternal(videoDecoderOutputBufferRenderer); + } + + @Override + public void clearVideoDecoderOutputBufferRenderer() { + verifyApplicationThread(); + setVideoDecoderOutputBufferRendererInternal(/* videoDecoderOutputBufferRenderer= */ null); + } + + @Override + public void clearVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + verifyApplicationThread(); + if (videoDecoderOutputBufferRenderer != null + && videoDecoderOutputBufferRenderer == this.videoDecoderOutputBufferRenderer) { + clearVideoDecoderOutputBufferRenderer(); + } + } + @Override public void addAudioListener(AudioListener listener) { audioListeners.add(listener); @@ -425,6 +676,9 @@ public class SimpleExoPlayer extends BasePlayer @Override public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) { verifyApplicationThread(); + if (playerReleased) { + return; + } if (!Util.areEqual(this.audioAttributes, audioAttributes)) { this.audioAttributes = audioAttributes; for (Renderer renderer : renderers) { @@ -441,11 +695,11 @@ public class SimpleExoPlayer extends BasePlayer } } + audioFocusManager.setAudioAttributes(handleAudioFocus ? audioAttributes : null); + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand - int playerCommand = - audioFocusManager.setAudioAttributes( - handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); } @Override @@ -498,12 +752,12 @@ public class SimpleExoPlayer extends BasePlayer /** * Sets the stream type for audio playback, used by the underlying audio track. - *

    - * Setting the stream type during playback may introduce a short gap in audio output as the audio - * track is recreated. A new audio session id will also be generated. - *

    - * Calling this method overwrites any attributes set previously by calling - * {@link #setAudioAttributes(AudioAttributes)}. + * + *

    Setting the stream type during playback may introduce a short gap in audio output as the + * audio track is recreated. A new audio session id will also be generated. + * + *

    Calling this method overwrites any attributes set previously by calling {@link + * #setAudioAttributes(AudioAttributes)}. * * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}. * @param streamType The stream type for audio playback. @@ -552,6 +806,25 @@ public class SimpleExoPlayer extends BasePlayer analyticsCollector.removeListener(listener); } + /** + * Sets whether the player should pause automatically when audio is rerouted from a headset to + * device speakers. See the audio + * becoming noisy documentation for more information. + * + *

    This feature is not enabled by default. + * + * @param handleAudioBecomingNoisy Whether the player should pause automatically when audio is + * rerouted from a headset to device speakers. + */ + public void setHandleAudioBecomingNoisy(boolean handleAudioBecomingNoisy) { + verifyApplicationThread(); + if (playerReleased) { + return; + } + audioBecomingNoisyManager.setEnabled(handleAudioBecomingNoisy); + } + /** * Sets a {@link PriorityTaskManager}, or null to clear a previously set priority task manager. * @@ -882,11 +1155,13 @@ public class SimpleExoPlayer extends BasePlayer } @Override + @State public int getPlaybackState() { verifyApplicationThread(); return player.getPlaybackState(); } + @Override @PlaybackSuppressionReason public int getPlaybackSuppressionReason() { verifyApplicationThread(); @@ -923,9 +1198,10 @@ public class SimpleExoPlayer extends BasePlayer } this.mediaSource = mediaSource; mediaSource.addEventListener(eventHandler, analyticsCollector); + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING); + updatePlayWhenReady(playWhenReady, playerCommand); player.prepare(mediaSource, resetPosition, resetState); } @@ -933,7 +1209,7 @@ public class SimpleExoPlayer extends BasePlayer public void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThread(); @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState()); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); updatePlayWhenReady(playWhenReady, playerCommand); } @@ -1012,6 +1288,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void stop(boolean reset) { verifyApplicationThread(); + audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE); player.stop(reset); if (mediaSource != null) { mediaSource.removeEventListener(analyticsCollector); @@ -1020,14 +1297,16 @@ public class SimpleExoPlayer extends BasePlayer mediaSource = null; } } - audioFocusManager.handleStop(); currentCues = Collections.emptyList(); } @Override public void release(boolean async) { verifyApplicationThread(); - audioFocusManager.handleStop(); + audioBecomingNoisyManager.setEnabled(false); + wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + audioFocusManager.release(); if (async) { Utilities.globalQueue.postRunnable(() -> player.release(async)); } else { @@ -1050,13 +1329,7 @@ public class SimpleExoPlayer extends BasePlayer } bandwidthMeter.removeEventListener(analyticsCollector); currentCues = Collections.emptyList(); - } - - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void sendMessages(ExoPlayerMessage... messages) { - player.sendMessages(messages); + playerReleased = true; } @Override @@ -1065,13 +1338,6 @@ public class SimpleExoPlayer extends BasePlayer return player.createMessage(target); } - @Override - @Deprecated - @SuppressWarnings("deprecation") - public void blockingSendMessages(ExoPlayerMessage... messages) { - player.blockingSendMessages(messages); - } - @Override public int getRendererCount() { verifyApplicationThread(); @@ -1102,13 +1368,6 @@ public class SimpleExoPlayer extends BasePlayer return player.getCurrentTimeline(); } - @Override - @Nullable - public Object getCurrentManifest() { - verifyApplicationThread(); - return player.getCurrentManifest(); - } - @Override public int getCurrentPeriodIndex() { verifyApplicationThread(); @@ -1175,6 +1434,62 @@ public class SimpleExoPlayer extends BasePlayer return player.getContentBufferedPosition(); } + /** + * Sets whether the player should use a {@link android.os.PowerManager.WakeLock} to ensure the + * device stays awake for playback, even when the screen is off. + * + *

    Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback can occur when the screen is off (e.g. background audio playback). It is not useful if + * the screen will always be on during playback (e.g. foreground video playback). + * + *

    This feature is not enabled by default. If enabled, a WakeLock is held whenever the player + * is in the {@link #STATE_READY READY} or {@link #STATE_BUFFERING BUFFERING} states with {@code + * playWhenReady = true}. + * + * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock} + * to ensure the device stays awake for playback, even when the screen is off. + * @deprecated Use {@link #setWakeMode(int)} instead. + */ + @Deprecated + public void setHandleWakeLock(boolean handleWakeLock) { + setWakeMode(handleWakeLock ? C.WAKE_MODE_LOCAL : C.WAKE_MODE_NONE); + } + + /** + * Sets how the player should keep the device awake for playback when the screen is off. + * + *

    Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback occurs and the screen is off (e.g. background audio playback). It is not useful when + * the screen will be kept on during playback (e.g. foreground video playback). + * + *

    When enabled, the locks ({@link android.os.PowerManager.WakeLock} / {@link + * android.net.wifi.WifiManager.WifiLock}) will be held whenever the player is in the {@link + * #STATE_READY} or {@link #STATE_BUFFERING} states with {@code playWhenReady = true}. The locks + * held depends on the specified {@link C.WakeMode}. + * + * @param wakeMode The {@link C.WakeMode} option to keep the device awake during playback. + */ + public void setWakeMode(@C.WakeMode int wakeMode) { + switch (wakeMode) { + case C.WAKE_MODE_NONE: + wakeLockManager.setEnabled(false); + wifiLockManager.setEnabled(false); + break; + case C.WAKE_MODE_LOCAL: + wakeLockManager.setEnabled(true); + wifiLockManager.setEnabled(false); + break; + case C.WAKE_MODE_NETWORK: + wakeLockManager.setEnabled(true); + wifiLockManager.setEnabled(true); + break; + default: + break; + } + } + // Internal methods. private void removeSurfaceCallbacks() { @@ -1220,6 +1535,20 @@ public class SimpleExoPlayer extends BasePlayer this.ownsSurface = ownsSurface; } + private void setVideoDecoderOutputBufferRendererInternal( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) + .setPayload(videoDecoderOutputBufferRenderer) + .send(); + } + } + this.videoDecoderOutputBufferRenderer = videoDecoderOutputBufferRenderer; + } + private void maybeNotifySurfaceSizeChanged(int width, int height) { if (width != surfaceWidth || height != surfaceHeight) { surfaceWidth = width; @@ -1261,6 +1590,24 @@ public class SimpleExoPlayer extends BasePlayer } } + private void updateWakeAndWifiLock() { + @State int playbackState = getPlaybackState(); + switch (playbackState) { + case Player.STATE_READY: + case Player.STATE_BUFFERING: + wakeLockManager.setStayAwake(getPlayWhenReady()); + wifiLockManager.setStayAwake(getPlayWhenReady()); + break; + case Player.STATE_ENDED: + case Player.STATE_IDLE: + wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + break; + default: + throw new IllegalStateException(); + } + } + private final class ComponentListener implements VideoRendererEventListener, AudioRendererEventListener, @@ -1269,6 +1616,7 @@ public class SimpleExoPlayer extends BasePlayer SurfaceHolder.Callback, TextureView.SurfaceTextureListener, AudioFocusManager.PlayerControl, + AudioBecomingNoisyManager.EventListener, Player.EventListener { // VideoRendererEventListener implementation @@ -1282,11 +1630,11 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs) { + public void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { - videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, - initializationDurationMs); + videoDebugListener.onVideoDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs); } } @@ -1306,8 +1654,8 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { // Prevent duplicate notification if a listener is both a VideoRendererEventListener and // a VideoListener, as they have the same method signature. @@ -1317,8 +1665,8 @@ public class SimpleExoPlayer extends BasePlayer } } for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { - videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, - pixelWidthHeightRatio); + videoDebugListener.onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } } @@ -1372,11 +1720,11 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs) { + public void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { - audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, - initializationDurationMs); + audioDebugListener.onAudioDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs); } } @@ -1389,8 +1737,8 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, - long elapsedSinceLastFeedMs) { + public void onAudioSinkUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } @@ -1439,7 +1787,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void surfaceDestroyed(SurfaceHolder holder) { - setVideoSurfaceInternal(null, false); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); } @@ -1451,6 +1799,7 @@ public class SimpleExoPlayer extends BasePlayer setVideoSurfaceInternal(new Surface(surfaceTexture), true); needSetSurface = false; } + setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true); maybeNotifySurfaceSizeChanged(width, height); } @@ -1466,7 +1815,7 @@ public class SimpleExoPlayer extends BasePlayer return false; } } - setVideoSurfaceInternal(null, true); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); needSetSurface = true; return true; @@ -1492,6 +1841,13 @@ public class SimpleExoPlayer extends BasePlayer updatePlayWhenReady(getPlayWhenReady(), playerCommand); } + // AudioBecomingNoisyManager.EventListener implementation. + + @Override + public void onAudioBecomingNoisy() { + setPlayWhenReady(false); + } + // Player.EventListener implementation. @Override @@ -1506,5 +1862,10 @@ public class SimpleExoPlayer extends BasePlayer } } } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) { + updateWakeAndWifiLock(); + } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/Timeline.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/Timeline.java index 9c26b546b..4dac71559 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -15,10 +15,11 @@ */ package com.google.android.exoplayer2; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; /** * A flexible representation of the structure of media. A timeline is able to represent the @@ -26,102 +27,113 @@ import com.google.android.exoplayer2.util.Assertions; * complex compositions of media such as playlists and streams with inserted ads. Instances are * immutable. For cases where media is changing dynamically (e.g. live streams), a timeline provides * a snapshot of the current state. - *

    - * A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single - * logical piece of media, for example a media file. It may also define groups of ads inserted into - * the media, along with information about whether those ads have been loaded and played. A window - * spans one or more periods, defining the region within those periods that's currently available - * for playback along with additional information such as whether seeking is supported within the - * window. Each window defines a default position, which is the position from which playback will - * start when the player starts playing the window. The following examples illustrate timelines for - * various use cases. + * + *

    A timeline consists of {@link Window Windows} and {@link Period Periods}. + * + *

      + *
    • A {@link Window} usually corresponds to one playlist item. It may span one or more periods + * and it defines the region within those periods that's currently available for playback. The + * window also provides additional information such as whether seeking is supported within the + * window and the default position, which is the position from which playback will start when + * the player starts playing the window. + *
    • A {@link Period} defines a single logical piece of media, for example a media file. It may + * also define groups of ads inserted into the media, along with information about whether + * those ads have been loaded and played. + *
    + * + *

    The following examples illustrate timelines for various use cases. * *

    Single media file or on-demand stream

    - *

    - * Example timeline for a single file - *

    - * A timeline for a single media file or on-demand stream consists of a single period and window. - * The window spans the whole period, indicating that all parts of the media are available for - * playback. The window's default position is typically at the start of the period (indicated by the - * black dot in the figure above). + * + *

    Example timeline for a
+ * single file A timeline for a single media file or on-demand stream consists of a single period + * and window. The window spans the whole period, indicating that all parts of the media are + * available for playback. The window's default position is typically at the start of the period + * (indicated by the black dot in the figure above). * *

    Playlist of media files or on-demand streams

    - *

    - * Example timeline for a playlist of files - *

    - * A timeline for a playlist of media files or on-demand streams consists of multiple periods, each - * with its own window. Each window spans the whole of the corresponding period, and typically has a - * default position at the start of the period. The properties of the periods and windows (e.g. - * their durations and whether the window is seekable) will often only become known when the player - * starts buffering the corresponding file or stream. + * + *

    Example timeline for a
+ * playlist of files A timeline for a playlist of media files or on-demand streams consists of + * multiple periods, each with its own window. Each window spans the whole of the corresponding + * period, and typically has a default position at the start of the period. The properties of the + * periods and windows (e.g. their durations and whether the window is seekable) will often only + * become known when the player starts buffering the corresponding file or stream. * *

    Live stream with limited availability

    - *

    - * Example timeline for a live stream with
- *       limited availability - *

    - * A timeline for a live stream consists of a period whose duration is unknown, since it's - * continually extending as more content is broadcast. If content only remains available for a - * limited period of time then the window may start at a non-zero position, defining the region of - * content that can still be played. The window will have {@link Window#isDynamic} set to true if - * the stream is still live. Its default position is typically near to the live edge (indicated by - * the black dot in the figure above). + * + *

    Example timeline for
+ * a live stream with limited availability A timeline for a live stream consists of a period whose + * duration is unknown, since it's continually extending as more content is broadcast. If content + * only remains available for a limited period of time then the window may start at a non-zero + * position, defining the region of content that can still be played. The window will have {@link + * Window#isLive} set to true to indicate it's a live stream and {@link Window#isDynamic} set to + * true as long as we expect changes to the live window. Its default position is typically near to + * the live edge (indicated by the black dot in the figure above). * *

    Live stream with indefinite availability

    - *

    - * Example timeline for a live stream with
- *       indefinite availability - *

    - * A timeline for a live stream with indefinite availability is similar to the - * Live stream with limited availability case, except that the window - * starts at the beginning of the period to indicate that all of the previously broadcast content - * can still be played. + * + *

    Example timeline
+ * for a live stream with indefinite availability A timeline for a live stream with indefinite + * availability is similar to the Live stream with limited availability + * case, except that the window starts at the beginning of the period to indicate that all of the + * previously broadcast content can still be played. * *

    Live stream with multiple periods

    - *

    - * Example timeline for a live stream
- *       with multiple periods - *

    - * This case arises when a live stream is explicitly divided into separate periods, for example at - * content boundaries. This case is similar to the Live stream with limited - * availability case, except that the window may span more than one period. Multiple periods are - * also possible in the indefinite availability case. + * + *

    Example timeline
+ * for a live stream with multiple periods This case arises when a live stream is explicitly + * divided into separate periods, for example at content boundaries. This case is similar to the Live stream with limited availability case, except that the window may + * span more than one period. Multiple periods are also possible in the indefinite availability + * case. * *

    On-demand stream followed by live stream

    - *

    - * Example timeline for an on-demand stream
- *       followed by a live stream - *

    - * This case is the concatenation of the Single media file or on-demand - * stream and Live stream with multiple periods cases. When playback - * of the on-demand stream ends, playback of the live stream will start from its default position - * near the live edge. + * + *

    Example timeline for an
+ * on-demand stream followed by a live stream This case is the concatenation of the Single media file or on-demand stream and Live + * stream with multiple periods cases. When playback of the on-demand stream ends, playback of + * the live stream will start from its default position near the live edge. * *

    On-demand stream with mid-roll ads

    - *

    - * Example timeline for an on-demand
- *       stream with mid-roll ad groups - *

    - * This case includes mid-roll ad groups, which are defined as part of the timeline's single period. - * The period can be queried for information about the ad groups and the ads they contain. + * + *

    Example
+ * timeline for an on-demand stream with mid-roll ad groups This case includes mid-roll ad groups, + * which are defined as part of the timeline's single period. The period can be queried for + * information about the ad groups and the ads they contain. */ public abstract class Timeline { /** - * Holds information about a window in a {@link Timeline}. A window defines a region of media - * currently available for playback along with additional information such as whether seeking is - * supported within the window. The figure below shows some of the information defined by a - * window, as well as how this information relates to corresponding {@link Period}s in the - * timeline. - *

    - * Information defined by a timeline window - *

    + * Holds information about a window in a {@link Timeline}. A window usually corresponds to one + * playlist item and defines a region of media currently available for playback along with + * additional information such as whether seeking is supported within the window. The figure below + * shows some of the information defined by a window, as well as how this information relates to + * corresponding {@link Period Periods} in the timeline. + * + *

    Information defined by a
+   * timeline window */ public static final class Window { + /** + * A {@link #uid} for a window that must be used for single-window {@link Timeline Timelines}. + */ + public static final Object SINGLE_WINDOW_UID = new Object(); + + /** + * A unique identifier for the window. Single-window {@link Timeline Timelines} must use {@link + * #SINGLE_WINDOW_UID}. + */ + public Object uid; + /** A tag for the window. Not necessarily unique. */ @Nullable public Object tag; + /** The manifest of the window. May be {@code null}. */ + @Nullable public Object manifest; + /** * The start time of the presentation to which this window belongs in milliseconds since the * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only. @@ -148,8 +160,13 @@ public abstract class Timeline { public boolean isDynamic; /** - * The index of the first period that belongs to this window. + * Whether the media in this window is live. For informational purposes only. + * + *

    Check {@link #isDynamic} to know whether this window may still change. */ + public boolean isLive; + + /** The index of the first period that belongs to this window. */ public int firstPeriodIndex; /** @@ -176,23 +193,34 @@ public abstract class Timeline { */ public long positionInFirstPeriodUs; + /** Creates window. */ + public Window() { + uid = SINGLE_WINDOW_UID; + } + /** Sets the data held by this window. */ public Window set( + Object uid, @Nullable Object tag, + @Nullable Object manifest, long presentationStartTimeMs, long windowStartTimeMs, boolean isSeekable, boolean isDynamic, + boolean isLive, long defaultPositionUs, long durationUs, int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs) { + this.uid = uid; this.tag = tag; + this.manifest = manifest; this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; this.defaultPositionUs = defaultPositionUs; this.durationUs = durationUs; this.firstPeriodIndex = firstPeriodIndex; @@ -251,18 +279,60 @@ public abstract class Timeline { return positionInFirstPeriodUs; } + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + Window that = (Window) obj; + return Util.areEqual(uid, that.uid) + && Util.areEqual(tag, that.tag) + && Util.areEqual(manifest, that.manifest) + && presentationStartTimeMs == that.presentationStartTimeMs + && windowStartTimeMs == that.windowStartTimeMs + && isSeekable == that.isSeekable + && isDynamic == that.isDynamic + && isLive == that.isLive + && defaultPositionUs == that.defaultPositionUs + && durationUs == that.durationUs + && firstPeriodIndex == that.firstPeriodIndex + && lastPeriodIndex == that.lastPeriodIndex + && positionInFirstPeriodUs == that.positionInFirstPeriodUs; + } + + @Override + public int hashCode() { + int result = 7; + result = 31 * result + uid.hashCode(); + result = 31 * result + (tag == null ? 0 : tag.hashCode()); + result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); + result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); + result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); + result = 31 * result + (isSeekable ? 1 : 0); + result = 31 * result + (isDynamic ? 1 : 0); + result = 31 * result + (isLive ? 1 : 0); + result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); + result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); + result = 31 * result + firstPeriodIndex; + result = 31 * result + lastPeriodIndex; + result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32)); + return result; + } } /** * Holds information about a period in a {@link Timeline}. A period defines a single logical piece * of media, for example a media file. It may also define groups of ads inserted into the media, * along with information about whether those ads have been loaded and played. - *

    - * The figure below shows some of the information defined by a period, as well as how this + * + *

    The figure below shows some of the information defined by a period, as well as how this * information relates to a corresponding {@link Window} in the timeline. - *

    - * Information defined by a period - *

    + * + *

    Information defined by a
+   * period */ public static final class Period { @@ -396,7 +466,8 @@ public abstract class Timeline { * microseconds. * * @param adGroupIndex The ad group index. - * @return The time of the ad group at the index, in microseconds. + * @return The time of the ad group at the index relative to the start of the enclosing {@link + * Period}, in microseconds, or {@link C#TIME_END_OF_SOURCE} for a post-roll ad group. */ public long getAdGroupTimeUs(int adGroupIndex) { return adPlaybackState.adGroupTimesUs[adGroupIndex]; @@ -439,22 +510,23 @@ public abstract class Timeline { } /** - * Returns the index of the ad group at or before {@code positionUs}, if that ad group is - * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has - * no ads remaining to be played, or if there is no such ad group. + * Returns the index of the ad group at or before {@code positionUs} in the period, if that ad + * group is unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code + * positionUs} has no ads remaining to be played, or if there is no such ad group. * - * @param positionUs The position at or before which to find an ad group, in microseconds. + * @param positionUs The period position at or before which to find an ad group, in + * microseconds. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexForPositionUs(long positionUs) { - return adPlaybackState.getAdGroupIndexForPositionUs(positionUs); + return adPlaybackState.getAdGroupIndexForPositionUs(positionUs, durationUs); } /** - * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be - * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. + * Returns the index of the next ad group after {@code positionUs} in the period that has ads + * remaining to be played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * - * @param positionUs The position after which to find an ad group, in microseconds. + * @param positionUs The period position after which to find an ad group, in microseconds. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs) { @@ -506,6 +578,34 @@ public abstract class Timeline { return adPlaybackState.adResumePositionUs; } + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + Period that = (Period) obj; + return Util.areEqual(id, that.id) + && Util.areEqual(uid, that.uid) + && windowIndex == that.windowIndex + && durationUs == that.durationUs + && positionInWindowUs == that.positionInWindowUs + && Util.areEqual(adPlaybackState, that.adPlaybackState); + } + + @Override + public int hashCode() { + int result = 7; + result = 31 * result + (id == null ? 0 : id.hashCode()); + result = 31 * result + (uid == null ? 0 : uid.hashCode()); + result = 31 * result + windowIndex; + result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); + result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); + result = 31 * result + (adPlaybackState == null ? 0 : adPlaybackState.hashCode()); + return result; + } } /** An empty timeline. */ @@ -518,8 +618,7 @@ public abstract class Timeline { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { throw new IndexOutOfBoundsException(); } @@ -631,28 +730,20 @@ public abstract class Timeline { } /** - * Populates a {@link Window} with data for the window at the specified index. Does not populate - * {@link Window#tag}. + * Populates a {@link Window} with data for the window at the specified index. * * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. * @return The populated {@link Window}, for convenience. */ public final Window getWindow(int windowIndex, Window window) { - return getWindow(windowIndex, window, false); + return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0); } - /** - * Populates a {@link Window} with data for the window at the specified index. - * - * @param windowIndex The index of the window. - * @param window The {@link Window} to populate. Must not be null. - * @param setTag Whether {@link Window#tag} should be populated. If false, the field will be set - * to null. The caller should pass false for efficiency reasons unless the field is required. - * @return The populated {@link Window}, for convenience. - */ + /** @deprecated Use {@link #getWindow(int, Window)} instead. Tags will always be set. */ + @Deprecated public final Window getWindow(int windowIndex, Window window, boolean setTag) { - return getWindow(windowIndex, window, setTag, 0); + return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0); } /** @@ -660,14 +751,12 @@ public abstract class Timeline { * * @param windowIndex The index of the window. * @param window The {@link Window} to populate. Must not be null. - * @param setTag Whether {@link Window#tag} should be populated. If false, the field will be set - * to null. The caller should pass false for efficiency reasons unless the field is required. * @param defaultPositionProjectionUs A duration into the future that the populated window's * default start position should be projected. * @return The populated {@link Window}, for convenience. */ public abstract Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs); + int windowIndex, Window window, long defaultPositionProjectionUs); /** * Returns the number of periods in the timeline. @@ -748,7 +837,7 @@ public abstract class Timeline { long windowPositionUs, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, getWindowCount()); - getWindow(windowIndex, window, false, defaultPositionProjectionUs); + getWindow(windowIndex, window, defaultPositionProjectionUs); if (windowPositionUs == C.TIME_UNSET) { windowPositionUs = window.getDefaultPositionUs(); if (windowPositionUs == C.TIME_UNSET) { @@ -802,8 +891,8 @@ public abstract class Timeline { public abstract Period getPeriod(int periodIndex, Period period, boolean setIds); /** - * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET} - * if the period is not in the timeline. + * Returns the index of the period identified by its unique {@link Period#uid}, or {@link + * C#INDEX_UNSET} if the period is not in the timeline. * * @param uid A unique identifier for a period. * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found. @@ -817,4 +906,50 @@ public abstract class Timeline { * @return The unique id of the period. */ public abstract Object getUidOfPeriod(int periodIndex); + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Timeline)) { + return false; + } + Timeline other = (Timeline) obj; + if (other.getWindowCount() != getWindowCount() || other.getPeriodCount() != getPeriodCount()) { + return false; + } + Timeline.Window window = new Timeline.Window(); + Timeline.Period period = new Timeline.Period(); + Timeline.Window otherWindow = new Timeline.Window(); + Timeline.Period otherPeriod = new Timeline.Period(); + for (int i = 0; i < getWindowCount(); i++) { + if (!getWindow(i, window).equals(other.getWindow(i, otherWindow))) { + return false; + } + } + for (int i = 0; i < getPeriodCount(); i++) { + if (!getPeriod(i, period, /* setIds= */ true) + .equals(other.getPeriod(i, otherPeriod, /* setIds= */ true))) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + Window window = new Window(); + Period period = new Period(); + int result = 7; + result = 31 * result + getWindowCount(); + for (int i = 0; i < getWindowCount(); i++) { + result = 31 * result + getWindow(i, window).hashCode(); + } + result = 31 * result + getPeriodCount(); + for (int i = 0; i < getPeriodCount(); i++) { + result = 31 * result + getPeriod(i, period, /* setIds= */ true).hashCode(); + } + return result; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/WakeLockManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/WakeLockManager.java new file mode 100644 index 000000000..6de302d62 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/WakeLockManager.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Log; + +/** + * Handles a {@link WakeLock}. + * + *

    The handling of wake locks requires the {@link android.Manifest.permission#WAKE_LOCK} + * permission. + */ +/* package */ final class WakeLockManager { + + private static final String TAG = "WakeLockManager"; + private static final String WAKE_LOCK_TAG = "ExoPlayer:WakeLockManager"; + + @Nullable private final PowerManager powerManager; + @Nullable private WakeLock wakeLock; + private boolean enabled; + private boolean stayAwake; + + public WakeLockManager(Context context) { + powerManager = + (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE); + } + + /** + * Sets whether to enable the acquiring and releasing of the {@link WakeLock}. + * + *

    By default, wake lock handling is not enabled. Enabling this will acquire the wake lock if + * necessary. Disabling this will release the wake lock if it is held. + * + *

    Enabling {@link WakeLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. + */ + public void setEnabled(boolean enabled) { + if (enabled) { + if (wakeLock == null) { + if (powerManager == null) { + Log.w(TAG, "PowerManager is null, therefore not creating the WakeLock."); + return; + } + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); + wakeLock.setReferenceCounted(false); + } + } + + this.enabled = enabled; + updateWakeLock(); + } + + /** + * Sets whether to acquire or release the {@link WakeLock}. + * + *

    Please note this method requires wake lock handling to be enabled through setEnabled(boolean + * enable) to actually have an impact on the {@link WakeLock}. + * + * @param stayAwake True if the player should acquire the {@link WakeLock}. False if the player + * should release. + */ + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + updateWakeLock(); + } + + // WakelockTimeout suppressed because the time the wake lock is needed for is unknown (could be + // listening to radio with screen off for multiple hours), therefore we can not determine a + // reasonable timeout that would not affect the user. + @SuppressLint("WakelockTimeout") + private void updateWakeLock() { + if (wakeLock == null) { + return; + } + + if (enabled && stayAwake) { + wakeLock.acquire(); + } else { + wakeLock.release(); + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/WifiLockManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/WifiLockManager.java new file mode 100644 index 000000000..d3700a646 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/WifiLockManager.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import android.content.Context; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.WifiLock; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Log; + +/** + * Handles a {@link WifiLock} + * + *

    The handling of wifi locks requires the {@link android.Manifest.permission#WAKE_LOCK} + * permission. + */ +/* package */ final class WifiLockManager { + + private static final String TAG = "WifiLockManager"; + private static final String WIFI_LOCK_TAG = "ExoPlayer:WifiLockManager"; + + @Nullable private final WifiManager wifiManager; + @Nullable private WifiLock wifiLock; + private boolean enabled; + private boolean stayAwake; + + public WifiLockManager(Context context) { + wifiManager = + (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + } + + /** + * Sets whether to enable the usage of a {@link WifiLock}. + * + *

    By default, wifi lock handling is not enabled. Enabling will acquire the wifi lock if + * necessary. Disabling will release the wifi lock if held. + * + *

    Enabling {@link WifiLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WifiLock}. + */ + public void setEnabled(boolean enabled) { + if (enabled && wifiLock == null) { + if (wifiManager == null) { + Log.w(TAG, "WifiManager is null, therefore not creating the WifiLock."); + return; + } + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, WIFI_LOCK_TAG); + wifiLock.setReferenceCounted(false); + } + + this.enabled = enabled; + updateWifiLock(); + } + + /** + * Sets whether to acquire or release the {@link WifiLock}. + * + *

    The wifi lock will not be acquired unless handling has been enabled through {@link + * #setEnabled(boolean)}. + * + * @param stayAwake True if the player should acquire the {@link WifiLock}. False if it should + * release. + */ + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + updateWifiLock(); + } + + private void updateWifiLock() { + if (wifiLock == null) { + return; + } + + if (enabled && stayAwake) { + wifiLock.acquire(); + } else { + wifiLock.release(); + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 87c78e798..f3d2d903e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -15,10 +15,9 @@ */ package com.google.android.exoplayer2.analytics; -import androidx.annotation.Nullable; - import android.graphics.SurfaceTexture; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -70,23 +69,6 @@ public class AnalyticsCollector VideoListener, AudioListener { - /** Factory for an analytics collector. */ - public static class Factory { - - /** - * Creates an analytics collector for the specified player. - * - * @param player The {@link Player} for which data will be collected. Can be null, if the player - * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics - * collector. - * @param clock A {@link Clock} used to generate timestamps. - * @return An analytics collector. - */ - public AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Clock clock) { - return new AnalyticsCollector(player, clock); - } - } - private final CopyOnWriteArraySet listeners; private final Clock clock; private final Window window; @@ -95,17 +77,11 @@ public class AnalyticsCollector private @MonotonicNonNull Player player; /** - * Creates an analytics collector for the specified player. + * Creates an analytics collector. * - * @param player The {@link Player} for which data will be collected. Can be null, if the player - * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics - * collector. * @param clock A {@link Clock} used to generate timestamps. */ - protected AnalyticsCollector(@Nullable Player player, Clock clock) { - if (player != null) { - this.player = player; - } + public AnalyticsCollector(Clock clock) { this.clock = Assertions.checkNotNull(clock); listeners = new CopyOnWriteArraySet<>(); mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); @@ -450,8 +426,7 @@ public class AnalyticsCollector // having slightly different real times. @Override - public final void onTimelineChanged( - Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { + public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { mediaPeriodQueueTracker.onTimelineChanged(timeline); EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { @@ -477,7 +452,7 @@ public class AnalyticsCollector } @Override - public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState); @@ -519,10 +494,7 @@ public class AnalyticsCollector @Override public final void onPlayerError(ExoPlaybackException error) { - EventTime eventTime = - error.type == ExoPlaybackException.TYPE_SOURCE - ? generateLoadingMediaPeriodEventTime() - : generatePlayingMediaPeriodEventTime(); + EventTime eventTime = generateLastReportedPlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerError(eventTime, error); } @@ -717,8 +689,9 @@ public class AnalyticsCollector private final HashMap mediaPeriodIdToInfo; private final Period period; - private @Nullable MediaPeriodInfo lastReportedPlayingMediaPeriod; - private @Nullable MediaPeriodInfo readingMediaPeriod; + @Nullable private MediaPeriodInfo lastPlayingMediaPeriod; + @Nullable private MediaPeriodInfo lastReportedPlayingMediaPeriod; + @Nullable private MediaPeriodInfo readingMediaPeriod; private Timeline timeline; private boolean isSeeking; @@ -736,7 +709,8 @@ public class AnalyticsCollector * always return null to reflect the uncertainty about the current playing period. May also be * null, if the timeline is empty or no media period is active yet. */ - public @Nullable MediaPeriodInfo getPlayingMediaPeriod() { + @Nullable + public MediaPeriodInfo getPlayingMediaPeriod() { return mediaPeriodInfoQueue.isEmpty() || timeline.isEmpty() || isSeeking ? null : mediaPeriodInfoQueue.get(0); @@ -749,7 +723,8 @@ public class AnalyticsCollector * reported until the seek or preparation is processed. May be null, if no media period is * active yet. */ - public @Nullable MediaPeriodInfo getLastReportedPlayingMediaPeriod() { + @Nullable + public MediaPeriodInfo getLastReportedPlayingMediaPeriod() { return lastReportedPlayingMediaPeriod; } @@ -757,7 +732,8 @@ public class AnalyticsCollector * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player. * May be null, if the player is not reading a media period. */ - public @Nullable MediaPeriodInfo getReadingMediaPeriod() { + @Nullable + public MediaPeriodInfo getReadingMediaPeriod() { return readingMediaPeriod; } @@ -766,14 +742,16 @@ public class AnalyticsCollector * currently loading or will be the next one loading. May be null, if no media period is active * yet. */ - public @Nullable MediaPeriodInfo getLoadingMediaPeriod() { + @Nullable + public MediaPeriodInfo getLoadingMediaPeriod() { return mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1); } /** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */ - public @Nullable MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { + @Nullable + public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { return mediaPeriodIdToInfo.get(mediaPeriodId); } @@ -786,7 +764,8 @@ public class AnalyticsCollector * Tries to find an existing media period info from the specified window index. Only returns a * non-null media period info if there is a unique, unambiguous match. */ - public @Nullable MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { + @Nullable + public MediaPeriodInfo tryResolveWindowIndex(int windowIndex) { MediaPeriodInfo match = null; for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { MediaPeriodInfo info = mediaPeriodInfoQueue.get(i); @@ -805,7 +784,7 @@ public class AnalyticsCollector /** Updates the queue with a reported position discontinuity . */ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a reported timeline change. */ @@ -820,7 +799,7 @@ public class AnalyticsCollector readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline); } this.timeline = timeline; - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a reported start of seek. */ @@ -831,18 +810,23 @@ public class AnalyticsCollector /** Updates the queue with a reported processed seek. */ public void onSeekProcessed() { isSeeking = false; - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } /** Updates the queue with a newly created media period. */ public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { - boolean isInTimeline = timeline.getIndexOfPeriod(mediaPeriodId.periodUid) != C.INDEX_UNSET; + int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); + boolean isInTimeline = periodIndex != C.INDEX_UNSET; MediaPeriodInfo mediaPeriodInfo = - new MediaPeriodInfo(mediaPeriodId, isInTimeline ? timeline : Timeline.EMPTY, windowIndex); + new MediaPeriodInfo( + mediaPeriodId, + isInTimeline ? timeline : Timeline.EMPTY, + isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex); mediaPeriodInfoQueue.add(mediaPeriodInfo); mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); + lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); if (mediaPeriodInfoQueue.size() == 1 && !timeline.isEmpty()) { - updateLastReportedPlayingMediaPeriod(); + lastReportedPlayingMediaPeriod = lastPlayingMediaPeriod; } } @@ -860,6 +844,9 @@ public class AnalyticsCollector if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) { readingMediaPeriod = mediaPeriodInfoQueue.isEmpty() ? null : mediaPeriodInfoQueue.get(0); } + if (!mediaPeriodInfoQueue.isEmpty()) { + lastPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); + } return true; } @@ -868,12 +855,6 @@ public class AnalyticsCollector readingMediaPeriod = mediaPeriodIdToInfo.get(mediaPeriodId); } - private void updateLastReportedPlayingMediaPeriod() { - if (!mediaPeriodInfoQueue.isEmpty()) { - lastReportedPlayingMediaPeriod = mediaPeriodInfoQueue.get(0); - } - } - private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline( MediaPeriodInfo info, Timeline newTimeline) { int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 2ffafbe90..e16d92df9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.analytics; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -69,7 +69,7 @@ public interface AnalyticsListener { * Media period identifier for the media period this event belongs to, or {@code null} if the * event is not associated with a specific media period. */ - public final @Nullable MediaPeriodId mediaPeriodId; + @Nullable public final MediaPeriodId mediaPeriodId; /** * Position in the window or ad this event belongs to at the time of the event, in milliseconds. @@ -128,10 +128,10 @@ public interface AnalyticsListener { * * @param eventTime The event time. * @param playWhenReady Whether the playback will proceed when ready. - * @param playbackState One of the {@link Player}.STATE constants. + * @param playbackState The new {@link Player.State playback state}. */ default void onPlayerStateChanged( - EventTime eventTime, boolean playWhenReady, int playbackState) {} + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) {} /** * Called when playback suppression reason changed. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java new file mode 100644 index 000000000..04f3ba154 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.analytics; + +import android.util.Base64; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the + * timeline and also for each ad within the windows. + * + *

    Sessions are identified by Base64-encoded, URL-safe, random strings. + */ +public final class DefaultPlaybackSessionManager implements PlaybackSessionManager { + + private static final Random RANDOM = new Random(); + private static final int SESSION_ID_LENGTH = 12; + + private final Timeline.Window window; + private final Timeline.Period period; + private final HashMap sessions; + + private @MonotonicNonNull Listener listener; + private Timeline currentTimeline; + @Nullable private String currentSessionId; + + /** Creates session manager. */ + public DefaultPlaybackSessionManager() { + window = new Timeline.Window(); + period = new Timeline.Period(); + sessions = new HashMap<>(); + currentTimeline = Timeline.EMPTY; + } + + @Override + public void setListener(Listener listener) { + this.listener = listener; + } + + @Override + public synchronized String getSessionForMediaPeriodId( + Timeline timeline, MediaPeriodId mediaPeriodId) { + int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex; + return getOrAddSession(windowIndex, mediaPeriodId).sessionId; + } + + @Override + public synchronized boolean belongsToSession(EventTime eventTime, String sessionId) { + SessionDescriptor sessionDescriptor = sessions.get(sessionId); + if (sessionDescriptor == null) { + return false; + } + sessionDescriptor.maybeSetWindowSequenceNumber(eventTime.windowIndex, eventTime.mediaPeriodId); + return sessionDescriptor.belongsToSession(eventTime.windowIndex, eventTime.mediaPeriodId); + } + + @Override + public synchronized void updateSessions(EventTime eventTime) { + Assertions.checkNotNull(listener); + @Nullable SessionDescriptor currentSession = sessions.get(currentSessionId); + if (eventTime.mediaPeriodId != null && currentSession != null) { + // If we receive an event associated with a media period, then it needs to be either part of + // the current window if it's the first created media period, or a window that will be played + // in the future. Otherwise, we know that it belongs to a session that was already finished + // and we can ignore the event. + boolean isAlreadyFinished = + currentSession.windowSequenceNumber == C.INDEX_UNSET + ? currentSession.windowIndex != eventTime.windowIndex + : eventTime.mediaPeriodId.windowSequenceNumber < currentSession.windowSequenceNumber; + if (isAlreadyFinished) { + return; + } + } + SessionDescriptor eventSession = + getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + if (currentSessionId == null) { + currentSessionId = eventSession.sessionId; + } + if (!eventSession.isCreated) { + eventSession.isCreated = true; + listener.onSessionCreated(eventTime, eventSession.sessionId); + } + if (eventSession.sessionId.equals(currentSessionId) && !eventSession.isActive) { + eventSession.isActive = true; + listener.onSessionActive(eventTime, eventSession.sessionId); + } + } + + @Override + public synchronized void handleTimelineUpdate(EventTime eventTime) { + Assertions.checkNotNull(listener); + Timeline previousTimeline = currentTimeline; + currentTimeline = eventTime.timeline; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) { + iterator.remove(); + if (session.isCreated) { + if (session.sessionId.equals(currentSessionId)) { + currentSessionId = null; + } + listener.onSessionFinished( + eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false); + } + } + } + handlePositionDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL); + } + + @Override + public synchronized void handlePositionDiscontinuity( + EventTime eventTime, @DiscontinuityReason int reason) { + Assertions.checkNotNull(listener); + boolean hasAutomaticTransition = + reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + || reason == Player.DISCONTINUITY_REASON_AD_INSERTION; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + if (session.isFinishedAtEventTime(eventTime)) { + iterator.remove(); + if (session.isCreated) { + boolean isRemovingCurrentSession = session.sessionId.equals(currentSessionId); + boolean isAutomaticTransition = + hasAutomaticTransition && isRemovingCurrentSession && session.isActive; + if (isRemovingCurrentSession) { + currentSessionId = null; + } + listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition); + } + } + } + @Nullable SessionDescriptor previousSessionDescriptor = sessions.get(currentSessionId); + SessionDescriptor currentSessionDescriptor = + getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId); + currentSessionId = currentSessionDescriptor.sessionId; + if (eventTime.mediaPeriodId != null + && eventTime.mediaPeriodId.isAd() + && (previousSessionDescriptor == null + || previousSessionDescriptor.windowSequenceNumber + != eventTime.mediaPeriodId.windowSequenceNumber + || previousSessionDescriptor.adMediaPeriodId == null + || previousSessionDescriptor.adMediaPeriodId.adGroupIndex + != eventTime.mediaPeriodId.adGroupIndex + || previousSessionDescriptor.adMediaPeriodId.adIndexInAdGroup + != eventTime.mediaPeriodId.adIndexInAdGroup)) { + // New ad playback started. Find corresponding content session and notify ad playback started. + MediaPeriodId contentMediaPeriodId = + new MediaPeriodId( + eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber); + SessionDescriptor contentSession = + getOrAddSession(eventTime.windowIndex, contentMediaPeriodId); + if (contentSession.isCreated && currentSessionDescriptor.isCreated) { + listener.onAdPlaybackStarted( + eventTime, contentSession.sessionId, currentSessionDescriptor.sessionId); + } + } + } + + @Override + public void finishAllSessions(EventTime eventTime) { + currentSessionId = null; + Iterator iterator = sessions.values().iterator(); + while (iterator.hasNext()) { + SessionDescriptor session = iterator.next(); + iterator.remove(); + if (session.isCreated && listener != null) { + listener.onSessionFinished( + eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false); + } + } + } + + private SessionDescriptor getOrAddSession( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is + // null, there may be multiple matching sessions with different window sequence numbers or + // adMediaPeriodIds. The best match is the one with the smaller window sequence number, and for + // windows with ads, the content session is preferred over ad sessions. + SessionDescriptor bestMatch = null; + long bestMatchWindowSequenceNumber = Long.MAX_VALUE; + for (SessionDescriptor sessionDescriptor : sessions.values()) { + sessionDescriptor.maybeSetWindowSequenceNumber(windowIndex, mediaPeriodId); + if (sessionDescriptor.belongsToSession(windowIndex, mediaPeriodId)) { + long windowSequenceNumber = sessionDescriptor.windowSequenceNumber; + if (windowSequenceNumber == C.INDEX_UNSET + || windowSequenceNumber < bestMatchWindowSequenceNumber) { + bestMatch = sessionDescriptor; + bestMatchWindowSequenceNumber = windowSequenceNumber; + } else if (windowSequenceNumber == bestMatchWindowSequenceNumber + && Util.castNonNull(bestMatch).adMediaPeriodId != null + && sessionDescriptor.adMediaPeriodId != null) { + bestMatch = sessionDescriptor; + } + } + } + if (bestMatch == null) { + String sessionId = generateSessionId(); + bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId); + sessions.put(sessionId, bestMatch); + } + return bestMatch; + } + + private static String generateSessionId() { + byte[] randomBytes = new byte[SESSION_ID_LENGTH]; + RANDOM.nextBytes(randomBytes); + return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP); + } + + /** + * Descriptor for a session. + * + *

    The session may be described in one of three ways: + * + *

      + *
    • A window index with unset window sequence number and a null ad media period id + *
    • A content window with index and sequence number, but a null ad media period id. + *
    • An ad with all values set. + *
    + */ + private final class SessionDescriptor { + + private final String sessionId; + + private int windowIndex; + private long windowSequenceNumber; + private @MonotonicNonNull MediaPeriodId adMediaPeriodId; + + private boolean isCreated; + private boolean isActive; + + public SessionDescriptor( + String sessionId, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + this.sessionId = sessionId; + this.windowIndex = windowIndex; + this.windowSequenceNumber = + mediaPeriodId == null ? C.INDEX_UNSET : mediaPeriodId.windowSequenceNumber; + if (mediaPeriodId != null && mediaPeriodId.isAd()) { + this.adMediaPeriodId = mediaPeriodId; + } + } + + public boolean tryResolvingToNewTimeline(Timeline oldTimeline, Timeline newTimeline) { + windowIndex = resolveWindowIndexToNewTimeline(oldTimeline, newTimeline, windowIndex); + if (windowIndex == C.INDEX_UNSET) { + return false; + } + if (adMediaPeriodId == null) { + return true; + } + int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid); + return newPeriodIndex != C.INDEX_UNSET; + } + + public boolean belongsToSession( + int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) { + if (eventMediaPeriodId == null) { + // Events without concrete media period id are for all sessions of the same window. + return eventWindowIndex == windowIndex; + } + if (adMediaPeriodId == null) { + // If this is a content session, only events for content with the same window sequence + // number belong to this session. + return !eventMediaPeriodId.isAd() + && eventMediaPeriodId.windowSequenceNumber == windowSequenceNumber; + } + // If this is an ad session, only events for this ad belong to the session. + return eventMediaPeriodId.windowSequenceNumber == adMediaPeriodId.windowSequenceNumber + && eventMediaPeriodId.adGroupIndex == adMediaPeriodId.adGroupIndex + && eventMediaPeriodId.adIndexInAdGroup == adMediaPeriodId.adIndexInAdGroup; + } + + public void maybeSetWindowSequenceNumber( + int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) { + if (windowSequenceNumber == C.INDEX_UNSET + && eventWindowIndex == windowIndex + && eventMediaPeriodId != null) { + // Set window sequence number for this session as soon as we have one. + windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber; + } + } + + public boolean isFinishedAtEventTime(EventTime eventTime) { + if (windowSequenceNumber == C.INDEX_UNSET) { + // Sessions with unspecified window sequence number are kept until we know more. + return false; + } + if (eventTime.mediaPeriodId == null) { + // For event times without media period id (e.g. after seek to new window), we only keep + // sessions of this window. + return windowIndex != eventTime.windowIndex; + } + if (eventTime.mediaPeriodId.windowSequenceNumber > windowSequenceNumber) { + // All past window sequence numbers are finished. + return true; + } + if (adMediaPeriodId == null) { + // Current or future content is not finished. + return false; + } + int eventPeriodIndex = eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid); + int adPeriodIndex = eventTime.timeline.getIndexOfPeriod(adMediaPeriodId.periodUid); + if (eventTime.mediaPeriodId.windowSequenceNumber < adMediaPeriodId.windowSequenceNumber + || eventPeriodIndex < adPeriodIndex) { + // Ads in future windows or periods are not finished. + return false; + } + if (eventPeriodIndex > adPeriodIndex) { + // Ads in past periods are finished. + return true; + } + if (eventTime.mediaPeriodId.isAd()) { + int eventAdGroup = eventTime.mediaPeriodId.adGroupIndex; + int eventAdIndex = eventTime.mediaPeriodId.adIndexInAdGroup; + // Finished if event is for an ad after this one in the same period. + return eventAdGroup > adMediaPeriodId.adGroupIndex + || (eventAdGroup == adMediaPeriodId.adGroupIndex + && eventAdIndex > adMediaPeriodId.adIndexInAdGroup); + } else { + // Finished if the event is for content after this ad. + return eventTime.mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET + || eventTime.mediaPeriodId.nextAdGroupIndex > adMediaPeriodId.adGroupIndex; + } + } + + private int resolveWindowIndexToNewTimeline( + Timeline oldTimeline, Timeline newTimeline, int windowIndex) { + if (windowIndex >= oldTimeline.getWindowCount()) { + return windowIndex < newTimeline.getWindowCount() ? windowIndex : C.INDEX_UNSET; + } + oldTimeline.getWindow(windowIndex, window); + for (int periodIndex = window.firstPeriodIndex; + periodIndex <= window.lastPeriodIndex; + periodIndex++) { + Object periodUid = oldTimeline.getUidOfPeriod(periodIndex); + int newPeriodIndex = newTimeline.getIndexOfPeriod(periodUid); + if (newPeriodIndex != C.INDEX_UNSET) { + return newTimeline.getPeriod(newPeriodIndex, period).windowIndex; + } + } + return C.INDEX_UNSET; + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java new file mode 100644 index 000000000..704577912 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackSessionManager.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.analytics; + +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; + +/** + * Manager for active playback sessions. + * + *

    The manager keeps track of the association between window index and/or media period id to + * session identifier. + */ +public interface PlaybackSessionManager { + + /** A listener for session updates. */ + interface Listener { + + /** + * Called when a new session is created as a result of {@link #updateSessions(EventTime)}. + * + * @param eventTime The {@link EventTime} at which the session is created. + * @param sessionId The identifier of the new session. + */ + void onSessionCreated(EventTime eventTime, String sessionId); + + /** + * Called when a session becomes active, i.e. playing in the foreground. + * + * @param eventTime The {@link EventTime} at which the session becomes active. + * @param sessionId The identifier of the session. + */ + void onSessionActive(EventTime eventTime, String sessionId); + + /** + * Called when a session is interrupted by ad playback. + * + * @param eventTime The {@link EventTime} at which the ad playback starts. + * @param contentSessionId The session identifier of the content session. + * @param adSessionId The identifier of the ad session. + */ + void onAdPlaybackStarted(EventTime eventTime, String contentSessionId, String adSessionId); + + /** + * Called when a session is permanently finished. + * + * @param eventTime The {@link EventTime} at which the session finished. + * @param sessionId The identifier of the finished session. + * @param automaticTransitionToNextPlayback Whether the session finished because of an automatic + * transition to the next playback item. + */ + void onSessionFinished( + EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback); + } + + /** + * Sets the listener to be notified of session updates. Must be called before the session manager + * is used. + * + * @param listener The {@link Listener} to be notified of session updates. + */ + void setListener(Listener listener); + + /** + * Returns the session identifier for the given media period id. + * + *

    Note that this will reserve a new session identifier if it doesn't exist yet, but will not + * call any {@link Listener} callbacks. + * + * @param timeline The timeline, {@code mediaPeriodId} is part of. + * @param mediaPeriodId A {@link MediaPeriodId}. + */ + String getSessionForMediaPeriodId(Timeline timeline, MediaPeriodId mediaPeriodId); + + /** + * Returns whether an event time belong to a session. + * + * @param eventTime The {@link EventTime}. + * @param sessionId A session identifier. + * @return Whether the event belongs to the specified session. + */ + boolean belongsToSession(EventTime eventTime, String sessionId); + + /** + * Updates or creates sessions based on a player {@link EventTime}. + * + * @param eventTime The {@link EventTime}. + */ + void updateSessions(EventTime eventTime); + + /** + * Updates the session associations to a new timeline. + * + * @param eventTime The event time with the timeline change. + */ + void handleTimelineUpdate(EventTime eventTime); + + /** + * Handles a position discontinuity. + * + * @param eventTime The event time of the position discontinuity. + * @param reason The {@link DiscontinuityReason}. + */ + void handlePositionDiscontinuity(EventTime eventTime, @DiscontinuityReason int reason); + + /** + * Finishes all existing sessions and calls their respective {@link + * Listener#onSessionFinished(EventTime, String, boolean)} callback. + * + * @param eventTime The event time at which sessions are finished. + */ + void finishAllSessions(EventTime eventTime); +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java new file mode 100644 index 000000000..b370c893d --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStats.java @@ -0,0 +1,980 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.analytics; + +import android.os.SystemClock; +import android.util.Pair; +import androidx.annotation.IntDef; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collections; +import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** Statistics about playbacks. */ +public final class PlaybackStats { + + /** + * State of a playback. One of {@link #PLAYBACK_STATE_NOT_STARTED}, {@link + * #PLAYBACK_STATE_JOINING_FOREGROUND}, {@link #PLAYBACK_STATE_JOINING_BACKGROUND}, {@link + * #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED}, {@link #PLAYBACK_STATE_SEEKING}, + * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_PAUSED_BUFFERING}, {@link + * #PLAYBACK_STATE_SEEK_BUFFERING}, {@link #PLAYBACK_STATE_SUPPRESSED}, {@link + * #PLAYBACK_STATE_SUPPRESSED_BUFFERING}, {@link #PLAYBACK_STATE_ENDED}, {@link + * #PLAYBACK_STATE_STOPPED}, {@link #PLAYBACK_STATE_FAILED}, {@link + * #PLAYBACK_STATE_INTERRUPTED_BY_AD} or {@link #PLAYBACK_STATE_ABANDONED}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) + @IntDef({ + PLAYBACK_STATE_NOT_STARTED, + PLAYBACK_STATE_JOINING_BACKGROUND, + PLAYBACK_STATE_JOINING_FOREGROUND, + PLAYBACK_STATE_PLAYING, + PLAYBACK_STATE_PAUSED, + PLAYBACK_STATE_SEEKING, + PLAYBACK_STATE_BUFFERING, + PLAYBACK_STATE_PAUSED_BUFFERING, + PLAYBACK_STATE_SEEK_BUFFERING, + PLAYBACK_STATE_SUPPRESSED, + PLAYBACK_STATE_SUPPRESSED_BUFFERING, + PLAYBACK_STATE_ENDED, + PLAYBACK_STATE_STOPPED, + PLAYBACK_STATE_FAILED, + PLAYBACK_STATE_INTERRUPTED_BY_AD, + PLAYBACK_STATE_ABANDONED + }) + @interface PlaybackState {} + /** Playback has not started (initial state). */ + public static final int PLAYBACK_STATE_NOT_STARTED = 0; + /** Playback is buffering in the background for initial playback start. */ + public static final int PLAYBACK_STATE_JOINING_BACKGROUND = 1; + /** Playback is buffering in the foreground for initial playback start. */ + public static final int PLAYBACK_STATE_JOINING_FOREGROUND = 2; + /** Playback is actively playing. */ + public static final int PLAYBACK_STATE_PLAYING = 3; + /** Playback is paused but ready to play. */ + public static final int PLAYBACK_STATE_PAUSED = 4; + /** Playback is handling a seek. */ + public static final int PLAYBACK_STATE_SEEKING = 5; + /** Playback is buffering to resume active playback. */ + public static final int PLAYBACK_STATE_BUFFERING = 6; + /** Playback is buffering while paused. */ + public static final int PLAYBACK_STATE_PAUSED_BUFFERING = 7; + /** Playback is buffering after a seek. */ + public static final int PLAYBACK_STATE_SEEK_BUFFERING = 8; + /** Playback is suppressed (e.g. due to audio focus loss). */ + public static final int PLAYBACK_STATE_SUPPRESSED = 9; + /** Playback is suppressed (e.g. due to audio focus loss) while buffering to resume a playback. */ + public static final int PLAYBACK_STATE_SUPPRESSED_BUFFERING = 10; + /** Playback has reached the end of the media. */ + public static final int PLAYBACK_STATE_ENDED = 11; + /** Playback is stopped and can be restarted. */ + public static final int PLAYBACK_STATE_STOPPED = 12; + /** Playback is stopped due a fatal error and can be retried. */ + public static final int PLAYBACK_STATE_FAILED = 13; + /** Playback is interrupted by an ad. */ + public static final int PLAYBACK_STATE_INTERRUPTED_BY_AD = 14; + /** Playback is abandoned before reaching the end of the media. */ + public static final int PLAYBACK_STATE_ABANDONED = 15; + /** Total number of playback states. */ + /* package */ static final int PLAYBACK_STATE_COUNT = 16; + + /** Empty playback stats. */ + public static final PlaybackStats EMPTY = merge(/* nothing */ ); + + /** + * Returns the combined {@link PlaybackStats} for all input {@link PlaybackStats}. + * + *

    Note that the full history of events is not kept as the history only makes sense in the + * context of a single playback. + * + * @param playbackStats Array of {@link PlaybackStats} to combine. + * @return The combined {@link PlaybackStats}. + */ + public static PlaybackStats merge(PlaybackStats... playbackStats) { + int playbackCount = 0; + long[] playbackStateDurationsMs = new long[PLAYBACK_STATE_COUNT]; + long firstReportedTimeMs = C.TIME_UNSET; + int foregroundPlaybackCount = 0; + int abandonedBeforeReadyCount = 0; + int endedCount = 0; + int backgroundJoiningCount = 0; + long totalValidJoinTimeMs = C.TIME_UNSET; + int validJoinTimeCount = 0; + int totalPauseCount = 0; + int totalPauseBufferCount = 0; + int totalSeekCount = 0; + int totalRebufferCount = 0; + long maxRebufferTimeMs = C.TIME_UNSET; + int adPlaybackCount = 0; + long totalVideoFormatHeightTimeMs = 0; + long totalVideoFormatHeightTimeProduct = 0; + long totalVideoFormatBitrateTimeMs = 0; + long totalVideoFormatBitrateTimeProduct = 0; + long totalAudioFormatTimeMs = 0; + long totalAudioFormatBitrateTimeProduct = 0; + int initialVideoFormatHeightCount = 0; + int initialVideoFormatBitrateCount = 0; + int totalInitialVideoFormatHeight = C.LENGTH_UNSET; + long totalInitialVideoFormatBitrate = C.LENGTH_UNSET; + int initialAudioFormatBitrateCount = 0; + long totalInitialAudioFormatBitrate = C.LENGTH_UNSET; + long totalBandwidthTimeMs = 0; + long totalBandwidthBytes = 0; + long totalDroppedFrames = 0; + long totalAudioUnderruns = 0; + int fatalErrorPlaybackCount = 0; + int fatalErrorCount = 0; + int nonFatalErrorCount = 0; + for (PlaybackStats stats : playbackStats) { + playbackCount += stats.playbackCount; + for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { + playbackStateDurationsMs[i] += stats.playbackStateDurationsMs[i]; + } + if (firstReportedTimeMs == C.TIME_UNSET) { + firstReportedTimeMs = stats.firstReportedTimeMs; + } else if (stats.firstReportedTimeMs != C.TIME_UNSET) { + firstReportedTimeMs = Math.min(firstReportedTimeMs, stats.firstReportedTimeMs); + } + foregroundPlaybackCount += stats.foregroundPlaybackCount; + abandonedBeforeReadyCount += stats.abandonedBeforeReadyCount; + endedCount += stats.endedCount; + backgroundJoiningCount += stats.backgroundJoiningCount; + if (totalValidJoinTimeMs == C.TIME_UNSET) { + totalValidJoinTimeMs = stats.totalValidJoinTimeMs; + } else if (stats.totalValidJoinTimeMs != C.TIME_UNSET) { + totalValidJoinTimeMs += stats.totalValidJoinTimeMs; + } + validJoinTimeCount += stats.validJoinTimeCount; + totalPauseCount += stats.totalPauseCount; + totalPauseBufferCount += stats.totalPauseBufferCount; + totalSeekCount += stats.totalSeekCount; + totalRebufferCount += stats.totalRebufferCount; + if (maxRebufferTimeMs == C.TIME_UNSET) { + maxRebufferTimeMs = stats.maxRebufferTimeMs; + } else if (stats.maxRebufferTimeMs != C.TIME_UNSET) { + maxRebufferTimeMs = Math.max(maxRebufferTimeMs, stats.maxRebufferTimeMs); + } + adPlaybackCount += stats.adPlaybackCount; + totalVideoFormatHeightTimeMs += stats.totalVideoFormatHeightTimeMs; + totalVideoFormatHeightTimeProduct += stats.totalVideoFormatHeightTimeProduct; + totalVideoFormatBitrateTimeMs += stats.totalVideoFormatBitrateTimeMs; + totalVideoFormatBitrateTimeProduct += stats.totalVideoFormatBitrateTimeProduct; + totalAudioFormatTimeMs += stats.totalAudioFormatTimeMs; + totalAudioFormatBitrateTimeProduct += stats.totalAudioFormatBitrateTimeProduct; + initialVideoFormatHeightCount += stats.initialVideoFormatHeightCount; + initialVideoFormatBitrateCount += stats.initialVideoFormatBitrateCount; + if (totalInitialVideoFormatHeight == C.LENGTH_UNSET) { + totalInitialVideoFormatHeight = stats.totalInitialVideoFormatHeight; + } else if (stats.totalInitialVideoFormatHeight != C.LENGTH_UNSET) { + totalInitialVideoFormatHeight += stats.totalInitialVideoFormatHeight; + } + if (totalInitialVideoFormatBitrate == C.LENGTH_UNSET) { + totalInitialVideoFormatBitrate = stats.totalInitialVideoFormatBitrate; + } else if (stats.totalInitialVideoFormatBitrate != C.LENGTH_UNSET) { + totalInitialVideoFormatBitrate += stats.totalInitialVideoFormatBitrate; + } + initialAudioFormatBitrateCount += stats.initialAudioFormatBitrateCount; + if (totalInitialAudioFormatBitrate == C.LENGTH_UNSET) { + totalInitialAudioFormatBitrate = stats.totalInitialAudioFormatBitrate; + } else if (stats.totalInitialAudioFormatBitrate != C.LENGTH_UNSET) { + totalInitialAudioFormatBitrate += stats.totalInitialAudioFormatBitrate; + } + totalBandwidthTimeMs += stats.totalBandwidthTimeMs; + totalBandwidthBytes += stats.totalBandwidthBytes; + totalDroppedFrames += stats.totalDroppedFrames; + totalAudioUnderruns += stats.totalAudioUnderruns; + fatalErrorPlaybackCount += stats.fatalErrorPlaybackCount; + fatalErrorCount += stats.fatalErrorCount; + nonFatalErrorCount += stats.nonFatalErrorCount; + } + return new PlaybackStats( + playbackCount, + playbackStateDurationsMs, + /* playbackStateHistory */ Collections.emptyList(), + /* mediaTimeHistory= */ Collections.emptyList(), + firstReportedTimeMs, + foregroundPlaybackCount, + abandonedBeforeReadyCount, + endedCount, + backgroundJoiningCount, + totalValidJoinTimeMs, + validJoinTimeCount, + totalPauseCount, + totalPauseBufferCount, + totalSeekCount, + totalRebufferCount, + maxRebufferTimeMs, + adPlaybackCount, + /* videoFormatHistory= */ Collections.emptyList(), + /* audioFormatHistory= */ Collections.emptyList(), + totalVideoFormatHeightTimeMs, + totalVideoFormatHeightTimeProduct, + totalVideoFormatBitrateTimeMs, + totalVideoFormatBitrateTimeProduct, + totalAudioFormatTimeMs, + totalAudioFormatBitrateTimeProduct, + initialVideoFormatHeightCount, + initialVideoFormatBitrateCount, + totalInitialVideoFormatHeight, + totalInitialVideoFormatBitrate, + initialAudioFormatBitrateCount, + totalInitialAudioFormatBitrate, + totalBandwidthTimeMs, + totalBandwidthBytes, + totalDroppedFrames, + totalAudioUnderruns, + fatalErrorPlaybackCount, + fatalErrorCount, + nonFatalErrorCount, + /* fatalErrorHistory= */ Collections.emptyList(), + /* nonFatalErrorHistory= */ Collections.emptyList()); + } + + /** The number of individual playbacks for which these stats were collected. */ + public final int playbackCount; + + // Playback state stats. + + /** + * The playback state history as ordered pairs of the {@link EventTime} at which a state became + * active and the {@link PlaybackState}. + */ + public final List> playbackStateHistory; + /** + * The media time history as an ordered list of long[2] arrays with [0] being the realtime as + * returned by {@code SystemClock.elapsedRealtime()} and [1] being the media time at this + * realtime, in milliseconds. + */ + public final List mediaTimeHistory; + /** + * The elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} of the first + * reported playback event, or {@link C#TIME_UNSET} if no event has been reported. + */ + public final long firstReportedTimeMs; + /** The number of playbacks which were the active foreground playback at some point. */ + public final int foregroundPlaybackCount; + /** The number of playbacks which were abandoned before they were ready to play. */ + public final int abandonedBeforeReadyCount; + /** The number of playbacks which reached the ended state at least once. */ + public final int endedCount; + /** The number of playbacks which were pre-buffered in the background. */ + public final int backgroundJoiningCount; + /** + * The total time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if no valid + * join time could be determined. + * + *

    Note that this does not include background joining time. A join time may be invalid if the + * playback never reached {@link #PLAYBACK_STATE_PLAYING} or {@link #PLAYBACK_STATE_PAUSED}, or + * joining was interrupted by a seek, stop, or error state. + */ + public final long totalValidJoinTimeMs; + /** + * The number of playbacks with a valid join time as documented in {@link #totalValidJoinTimeMs}. + */ + public final int validJoinTimeCount; + /** The total number of times a playback has been paused. */ + public final int totalPauseCount; + /** The total number of times a playback has been paused while rebuffering. */ + public final int totalPauseBufferCount; + /** + * The total number of times a seek occurred. This includes seeks happening before playback + * resumed after another seek. + */ + public final int totalSeekCount; + /** + * The total number of times a rebuffer occurred. This excludes initial joining and buffering + * after seek. + */ + public final int totalRebufferCount; + /** + * The maximum time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} if no + * rebuffer occurred. + */ + public final long maxRebufferTimeMs; + /** The number of ad playbacks. */ + public final int adPlaybackCount; + + // Format stats. + + /** + * The video format history as ordered pairs of the {@link EventTime} at which a format started + * being used and the {@link Format}. The {@link Format} may be null if no video format was used. + */ + public final List> videoFormatHistory; + /** + * The audio format history as ordered pairs of the {@link EventTime} at which a format started + * being used and the {@link Format}. The {@link Format} may be null if no audio format was used. + */ + public final List> audioFormatHistory; + /** The total media time for which video format height data is available, in milliseconds. */ + public final long totalVideoFormatHeightTimeMs; + /** + * The accumulated sum of all video format heights, in pixels, times the time the format was used + * for playback, in milliseconds. + */ + public final long totalVideoFormatHeightTimeProduct; + /** The total media time for which video format bitrate data is available, in milliseconds. */ + public final long totalVideoFormatBitrateTimeMs; + /** + * The accumulated sum of all video format bitrates, in bits per second, times the time the format + * was used for playback, in milliseconds. + */ + public final long totalVideoFormatBitrateTimeProduct; + /** The total media time for which audio format data is available, in milliseconds. */ + public final long totalAudioFormatTimeMs; + /** + * The accumulated sum of all audio format bitrates, in bits per second, times the time the format + * was used for playback, in milliseconds. + */ + public final long totalAudioFormatBitrateTimeProduct; + /** The number of playbacks with initial video format height data. */ + public final int initialVideoFormatHeightCount; + /** The number of playbacks with initial video format bitrate data. */ + public final int initialVideoFormatBitrateCount; + /** + * The total initial video format height for all playbacks, in pixels, or {@link C#LENGTH_UNSET} + * if no initial video format data is available. + */ + public final int totalInitialVideoFormatHeight; + /** + * The total initial video format bitrate for all playbacks, in bits per second, or {@link + * C#LENGTH_UNSET} if no initial video format data is available. + */ + public final long totalInitialVideoFormatBitrate; + /** The number of playbacks with initial audio format bitrate data. */ + public final int initialAudioFormatBitrateCount; + /** + * The total initial audio format bitrate for all playbacks, in bits per second, or {@link + * C#LENGTH_UNSET} if no initial audio format data is available. + */ + public final long totalInitialAudioFormatBitrate; + + // Bandwidth stats. + + /** The total time for which bandwidth measurement data is available, in milliseconds. */ + public final long totalBandwidthTimeMs; + /** The total bytes transferred during {@link #totalBandwidthTimeMs}. */ + public final long totalBandwidthBytes; + + // Renderer quality stats. + + /** The total number of dropped video frames. */ + public final long totalDroppedFrames; + /** The total number of audio underruns. */ + public final long totalAudioUnderruns; + + // Error stats. + + /** + * The total number of playback with at least one fatal error. Errors are fatal if playback + * stopped due to this error. + */ + public final int fatalErrorPlaybackCount; + /** The total number of fatal errors. Errors are fatal if playback stopped due to this error. */ + public final int fatalErrorCount; + /** + * The total number of non-fatal errors. Error are non-fatal if playback can recover from the + * error without stopping. + */ + public final int nonFatalErrorCount; + /** + * The history of fatal errors as ordered pairs of the {@link EventTime} at which an error + * occurred and the error. Errors are fatal if playback stopped due to this error. + */ + public final List> fatalErrorHistory; + /** + * The history of non-fatal errors as ordered pairs of the {@link EventTime} at which an error + * occurred and the error. Error are non-fatal if playback can recover from the error without + * stopping. + */ + public final List> nonFatalErrorHistory; + + private final long[] playbackStateDurationsMs; + + /* package */ PlaybackStats( + int playbackCount, + long[] playbackStateDurationsMs, + List> playbackStateHistory, + List mediaTimeHistory, + long firstReportedTimeMs, + int foregroundPlaybackCount, + int abandonedBeforeReadyCount, + int endedCount, + int backgroundJoiningCount, + long totalValidJoinTimeMs, + int validJoinTimeCount, + int totalPauseCount, + int totalPauseBufferCount, + int totalSeekCount, + int totalRebufferCount, + long maxRebufferTimeMs, + int adPlaybackCount, + List> videoFormatHistory, + List> audioFormatHistory, + long totalVideoFormatHeightTimeMs, + long totalVideoFormatHeightTimeProduct, + long totalVideoFormatBitrateTimeMs, + long totalVideoFormatBitrateTimeProduct, + long totalAudioFormatTimeMs, + long totalAudioFormatBitrateTimeProduct, + int initialVideoFormatHeightCount, + int initialVideoFormatBitrateCount, + int totalInitialVideoFormatHeight, + long totalInitialVideoFormatBitrate, + int initialAudioFormatBitrateCount, + long totalInitialAudioFormatBitrate, + long totalBandwidthTimeMs, + long totalBandwidthBytes, + long totalDroppedFrames, + long totalAudioUnderruns, + int fatalErrorPlaybackCount, + int fatalErrorCount, + int nonFatalErrorCount, + List> fatalErrorHistory, + List> nonFatalErrorHistory) { + this.playbackCount = playbackCount; + this.playbackStateDurationsMs = playbackStateDurationsMs; + this.playbackStateHistory = Collections.unmodifiableList(playbackStateHistory); + this.mediaTimeHistory = Collections.unmodifiableList(mediaTimeHistory); + this.firstReportedTimeMs = firstReportedTimeMs; + this.foregroundPlaybackCount = foregroundPlaybackCount; + this.abandonedBeforeReadyCount = abandonedBeforeReadyCount; + this.endedCount = endedCount; + this.backgroundJoiningCount = backgroundJoiningCount; + this.totalValidJoinTimeMs = totalValidJoinTimeMs; + this.validJoinTimeCount = validJoinTimeCount; + this.totalPauseCount = totalPauseCount; + this.totalPauseBufferCount = totalPauseBufferCount; + this.totalSeekCount = totalSeekCount; + this.totalRebufferCount = totalRebufferCount; + this.maxRebufferTimeMs = maxRebufferTimeMs; + this.adPlaybackCount = adPlaybackCount; + this.videoFormatHistory = Collections.unmodifiableList(videoFormatHistory); + this.audioFormatHistory = Collections.unmodifiableList(audioFormatHistory); + this.totalVideoFormatHeightTimeMs = totalVideoFormatHeightTimeMs; + this.totalVideoFormatHeightTimeProduct = totalVideoFormatHeightTimeProduct; + this.totalVideoFormatBitrateTimeMs = totalVideoFormatBitrateTimeMs; + this.totalVideoFormatBitrateTimeProduct = totalVideoFormatBitrateTimeProduct; + this.totalAudioFormatTimeMs = totalAudioFormatTimeMs; + this.totalAudioFormatBitrateTimeProduct = totalAudioFormatBitrateTimeProduct; + this.initialVideoFormatHeightCount = initialVideoFormatHeightCount; + this.initialVideoFormatBitrateCount = initialVideoFormatBitrateCount; + this.totalInitialVideoFormatHeight = totalInitialVideoFormatHeight; + this.totalInitialVideoFormatBitrate = totalInitialVideoFormatBitrate; + this.initialAudioFormatBitrateCount = initialAudioFormatBitrateCount; + this.totalInitialAudioFormatBitrate = totalInitialAudioFormatBitrate; + this.totalBandwidthTimeMs = totalBandwidthTimeMs; + this.totalBandwidthBytes = totalBandwidthBytes; + this.totalDroppedFrames = totalDroppedFrames; + this.totalAudioUnderruns = totalAudioUnderruns; + this.fatalErrorPlaybackCount = fatalErrorPlaybackCount; + this.fatalErrorCount = fatalErrorCount; + this.nonFatalErrorCount = nonFatalErrorCount; + this.fatalErrorHistory = Collections.unmodifiableList(fatalErrorHistory); + this.nonFatalErrorHistory = Collections.unmodifiableList(nonFatalErrorHistory); + } + + /** + * Returns the total time spent in a given {@link PlaybackState}, in milliseconds. + * + * @param playbackState A {@link PlaybackState}. + * @return Total spent in the given playback state, in milliseconds + */ + public long getPlaybackStateDurationMs(@PlaybackState int playbackState) { + return playbackStateDurationsMs[playbackState]; + } + + /** + * Returns the {@link PlaybackState} at the given time. + * + * @param realtimeMs The time as returned by {@link SystemClock#elapsedRealtime()}. + * @return The {@link PlaybackState} at that time, or {@link #PLAYBACK_STATE_NOT_STARTED} if the + * given time is before the first known playback state in the history. + */ + public @PlaybackState int getPlaybackStateAtTime(long realtimeMs) { + @PlaybackState int state = PLAYBACK_STATE_NOT_STARTED; + for (Pair timeAndState : playbackStateHistory) { + if (timeAndState.first.realtimeMs > realtimeMs) { + break; + } + state = timeAndState.second; + } + return state; + } + + /** + * Returns the estimated media time at the given realtime, in milliseconds, or {@link + * C#TIME_UNSET} if the media time history is unknown. + * + * @param realtimeMs The realtime as returned by {@link SystemClock#elapsedRealtime()}. + * @return The estimated media time in milliseconds at this realtime, {@link C#TIME_UNSET} if no + * estimate can be given. + */ + public long getMediaTimeMsAtRealtimeMs(long realtimeMs) { + if (mediaTimeHistory.isEmpty()) { + return C.TIME_UNSET; + } + int nextIndex = 0; + while (nextIndex < mediaTimeHistory.size() + && mediaTimeHistory.get(nextIndex)[0] <= realtimeMs) { + nextIndex++; + } + if (nextIndex == 0) { + return mediaTimeHistory.get(0)[1]; + } + if (nextIndex == mediaTimeHistory.size()) { + return mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; + } + long prevRealtimeMs = mediaTimeHistory.get(nextIndex - 1)[0]; + long prevMediaTimeMs = mediaTimeHistory.get(nextIndex - 1)[1]; + long nextRealtimeMs = mediaTimeHistory.get(nextIndex)[0]; + long nextMediaTimeMs = mediaTimeHistory.get(nextIndex)[1]; + long realtimeDurationMs = nextRealtimeMs - prevRealtimeMs; + if (realtimeDurationMs == 0) { + return prevMediaTimeMs; + } + float fraction = (float) (realtimeMs - prevRealtimeMs) / realtimeDurationMs; + return prevMediaTimeMs + (long) ((nextMediaTimeMs - prevMediaTimeMs) * fraction); + } + + /** + * Returns the mean time spent joining the playback, in milliseconds, or {@link C#TIME_UNSET} if + * no valid join time is available. Only includes playbacks with valid join times as documented in + * {@link #totalValidJoinTimeMs}. + */ + public long getMeanJoinTimeMs() { + return validJoinTimeCount == 0 ? C.TIME_UNSET : totalValidJoinTimeMs / validJoinTimeCount; + } + + /** + * Returns the total time spent joining the playback in foreground, in milliseconds. This does + * include invalid join times where the playback never reached {@link #PLAYBACK_STATE_PLAYING} or + * {@link #PLAYBACK_STATE_PAUSED}, or joining was interrupted by a seek, stop, or error state. + */ + public long getTotalJoinTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND); + } + + /** Returns the total time spent actively playing, in milliseconds. */ + public long getTotalPlayTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_PLAYING); + } + + /** + * Returns the mean time spent actively playing per foreground playback, in milliseconds, or + * {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPlayTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPlayTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time spent in a paused state, in milliseconds. */ + public long getTotalPausedTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED) + + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING); + } + + /** + * Returns the mean time spent in a paused state per foreground playback, in milliseconds, or + * {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPausedTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPausedTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the total time spent rebuffering, in milliseconds. This excludes initial join times, + * buffer times after a seek and buffering while paused. + */ + public long getTotalRebufferTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING); + } + + /** + * Returns the mean time spent rebuffering per foreground playback, in milliseconds, or {@link + * C#TIME_UNSET} if no playback has been in foreground. This excludes initial join times, buffer + * times after a seek and buffering while paused. + */ + public long getMeanRebufferTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalRebufferTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the mean time spent during a single rebuffer, in milliseconds, or {@link C#TIME_UNSET} + * if no rebuffer was recorded. This excludes initial join times and buffer times after a seek. + */ + public long getMeanSingleRebufferTimeMs() { + return totalRebufferCount == 0 + ? C.TIME_UNSET + : (getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_PAUSED_BUFFERING)) + / totalRebufferCount; + } + + /** + * Returns the total time spent from the start of a seek until playback is ready again, in + * milliseconds. + */ + public long getTotalSeekTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + } + + /** + * Returns the mean time spent per foreground playback from the start of a seek until playback is + * ready again, in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanSeekTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalSeekTimeMs() / foregroundPlaybackCount; + } + + /** + * Returns the mean time spent from the start of a single seek until playback is ready again, in + * milliseconds, or {@link C#TIME_UNSET} if no seek occurred. + */ + public long getMeanSingleSeekTimeMs() { + return totalSeekCount == 0 ? C.TIME_UNSET : getTotalSeekTimeMs() / totalSeekCount; + } + + /** + * Returns the total time spent actively waiting for playback, in milliseconds. This includes all + * join times, rebuffer times and seek times, but excludes times without user intention to play, + * e.g. all paused states. + */ + public long getTotalWaitTimeMs() { + return getPlaybackStateDurationMs(PLAYBACK_STATE_JOINING_FOREGROUND) + + getPlaybackStateDurationMs(PLAYBACK_STATE_BUFFERING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEKING) + + getPlaybackStateDurationMs(PLAYBACK_STATE_SEEK_BUFFERING); + } + + /** + * Returns the mean time spent actively waiting for playback per foreground playback, in + * milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. This includes all + * join times, rebuffer times and seek times, but excludes times without user intention to play, + * e.g. all paused states. + */ + public long getMeanWaitTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalWaitTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time spent playing or actively waiting for playback, in milliseconds. */ + public long getTotalPlayAndWaitTimeMs() { + return getTotalPlayTimeMs() + getTotalWaitTimeMs(); + } + + /** + * Returns the mean time spent playing or actively waiting for playback per foreground playback, + * in milliseconds, or {@link C#TIME_UNSET} if no playback has been in foreground. + */ + public long getMeanPlayAndWaitTimeMs() { + return foregroundPlaybackCount == 0 + ? C.TIME_UNSET + : getTotalPlayAndWaitTimeMs() / foregroundPlaybackCount; + } + + /** Returns the total time covered by any playback state, in milliseconds. */ + public long getTotalElapsedTimeMs() { + long totalTimeMs = 0; + for (int i = 0; i < PLAYBACK_STATE_COUNT; i++) { + totalTimeMs += playbackStateDurationsMs[i]; + } + return totalTimeMs; + } + + /** + * Returns the mean time covered by any playback state per playback, in milliseconds, or {@link + * C#TIME_UNSET} if no playback was recorded. + */ + public long getMeanElapsedTimeMs() { + return playbackCount == 0 ? C.TIME_UNSET : getTotalElapsedTimeMs() / playbackCount; + } + + /** + * Returns the ratio of foreground playbacks which were abandoned before they were ready to play, + * or {@code 0.0} if no playback has been in foreground. + */ + public float getAbandonedBeforeReadyRatio() { + int foregroundAbandonedBeforeReady = + abandonedBeforeReadyCount - (playbackCount - foregroundPlaybackCount); + return foregroundPlaybackCount == 0 + ? 0f + : (float) foregroundAbandonedBeforeReady / foregroundPlaybackCount; + } + + /** + * Returns the ratio of foreground playbacks which reached the ended state at least once, or + * {@code 0.0} if no playback has been in foreground. + */ + public float getEndedRatio() { + return foregroundPlaybackCount == 0 ? 0f : (float) endedCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a playback has been paused per foreground playback, or {@code + * 0.0} if no playback has been in foreground. + */ + public float getMeanPauseCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalPauseCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a playback has been paused while rebuffering per foreground + * playback, or {@code 0.0} if no playback has been in foreground. + */ + public float getMeanPauseBufferCount() { + return foregroundPlaybackCount == 0 + ? 0f + : (float) totalPauseBufferCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a seek occurred per foreground playback, or {@code 0.0} if no + * playback has been in foreground. This includes seeks happening before playback resumed after + * another seek. + */ + public float getMeanSeekCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalSeekCount / foregroundPlaybackCount; + } + + /** + * Returns the mean number of times a rebuffer occurred per foreground playback, or {@code 0.0} if + * no playback has been in foreground. This excludes initial joining and buffering after seek. + */ + public float getMeanRebufferCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) totalRebufferCount / foregroundPlaybackCount; + } + + /** + * Returns the ratio of wait times to the total time spent playing and waiting, or {@code 0.0} if + * no time was spend playing or waiting. This is equivalent to {@link #getTotalWaitTimeMs()} / + * {@link #getTotalPlayAndWaitTimeMs()} and also to {@link #getJoinTimeRatio()} + {@link + * #getRebufferTimeRatio()} + {@link #getSeekTimeRatio()}. + */ + public float getWaitTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalWaitTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of foreground join time to the total time spent playing and waiting, or + * {@code 0.0} if no time was spend playing or waiting. This is equivalent to {@link + * #getTotalJoinTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getJoinTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalJoinTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of rebuffer time to the total time spent playing and waiting, or {@code 0.0} + * if no time was spend playing or waiting. This is equivalent to {@link + * #getTotalRebufferTimeMs()} / {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getRebufferTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalRebufferTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the ratio of seek time to the total time spent playing and waiting, or {@code 0.0} if + * no time was spend playing or waiting. This is equivalent to {@link #getTotalSeekTimeMs()} / + * {@link #getTotalPlayAndWaitTimeMs()}. + */ + public float getSeekTimeRatio() { + long playAndWaitTimeMs = getTotalPlayAndWaitTimeMs(); + return playAndWaitTimeMs == 0 ? 0f : (float) getTotalSeekTimeMs() / playAndWaitTimeMs; + } + + /** + * Returns the rate of rebuffer events, in rebuffers per play time second, or {@code 0.0} if no + * time was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenRebuffers()}. + */ + public float getRebufferRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalRebufferCount / playTimeMs; + } + + /** + * Returns the mean play time between rebuffer events, in seconds. This is equivalent to 1.0 / + * {@link #getRebufferRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenRebuffers() { + return 1f / getRebufferRate(); + } + + /** + * Returns the mean initial video format height, in pixels, or {@link C#LENGTH_UNSET} if no video + * format data is available. + */ + public int getMeanInitialVideoFormatHeight() { + return initialVideoFormatHeightCount == 0 + ? C.LENGTH_UNSET + : totalInitialVideoFormatHeight / initialVideoFormatHeightCount; + } + + /** + * Returns the mean initial video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if + * no video format data is available. + */ + public int getMeanInitialVideoFormatBitrate() { + return initialVideoFormatBitrateCount == 0 + ? C.LENGTH_UNSET + : (int) (totalInitialVideoFormatBitrate / initialVideoFormatBitrateCount); + } + + /** + * Returns the mean initial audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if + * no audio format data is available. + */ + public int getMeanInitialAudioFormatBitrate() { + return initialAudioFormatBitrateCount == 0 + ? C.LENGTH_UNSET + : (int) (totalInitialAudioFormatBitrate / initialAudioFormatBitrateCount); + } + + /** + * Returns the mean video format height, in pixels, or {@link C#LENGTH_UNSET} if no video format + * data is available. This is a weighted average taking the time the format was used for playback + * into account. + */ + public int getMeanVideoFormatHeight() { + return totalVideoFormatHeightTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalVideoFormatHeightTimeProduct / totalVideoFormatHeightTimeMs); + } + + /** + * Returns the mean video format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no + * video format data is available. This is a weighted average taking the time the format was used + * for playback into account. + */ + public int getMeanVideoFormatBitrate() { + return totalVideoFormatBitrateTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalVideoFormatBitrateTimeProduct / totalVideoFormatBitrateTimeMs); + } + + /** + * Returns the mean audio format bitrate, in bits per second, or {@link C#LENGTH_UNSET} if no + * audio format data is available. This is a weighted average taking the time the format was used + * for playback into account. + */ + public int getMeanAudioFormatBitrate() { + return totalAudioFormatTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalAudioFormatBitrateTimeProduct / totalAudioFormatTimeMs); + } + + /** + * Returns the mean network bandwidth based on transfer measurements, in bits per second, or + * {@link C#LENGTH_UNSET} if no transfer data is available. + */ + public int getMeanBandwidth() { + return totalBandwidthTimeMs == 0 + ? C.LENGTH_UNSET + : (int) (totalBandwidthBytes * 8000 / totalBandwidthTimeMs); + } + + /** + * Returns the mean rate at which video frames are dropped, in dropped frames per play time + * second, or {@code 0.0} if no time was spent playing. + */ + public float getDroppedFramesRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalDroppedFrames / playTimeMs; + } + + /** + * Returns the mean rate at which audio underruns occurred, in underruns per play time second, or + * {@code 0.0} if no time was spent playing. + */ + public float getAudioUnderrunRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * totalAudioUnderruns / playTimeMs; + } + + /** + * Returns the ratio of foreground playbacks which experienced fatal errors, or {@code 0.0} if no + * playback has been in foreground. + */ + public float getFatalErrorRatio() { + return foregroundPlaybackCount == 0 + ? 0f + : (float) fatalErrorPlaybackCount / foregroundPlaybackCount; + } + + /** + * Returns the rate of fatal errors, in errors per play time second, or {@code 0.0} if no time was + * spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenFatalErrors()}. + */ + public float getFatalErrorRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * fatalErrorCount / playTimeMs; + } + + /** + * Returns the mean play time between fatal errors, in seconds. This is equivalent to 1.0 / {@link + * #getFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenFatalErrors() { + return 1f / getFatalErrorRate(); + } + + /** + * Returns the mean number of non-fatal errors per foreground playback, or {@code 0.0} if no + * playback has been in foreground. + */ + public float getMeanNonFatalErrorCount() { + return foregroundPlaybackCount == 0 ? 0f : (float) nonFatalErrorCount / foregroundPlaybackCount; + } + + /** + * Returns the rate of non-fatal errors, in errors per play time second, or {@code 0.0} if no time + * was spend playing. This is equivalent to 1.0 / {@link #getMeanTimeBetweenNonFatalErrors()}. + */ + public float getNonFatalErrorRate() { + long playTimeMs = getTotalPlayTimeMs(); + return playTimeMs == 0 ? 0f : 1000f * nonFatalErrorCount / playTimeMs; + } + + /** + * Returns the mean play time between non-fatal errors, in seconds. This is equivalent to 1.0 / + * {@link #getNonFatalErrorRate()}. Note that this may return {@link Float#POSITIVE_INFINITY}. + */ + public float getMeanTimeBetweenNonFatalErrors() { + return 1f / getNonFatalErrorRate(); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java new file mode 100644 index 000000000..46c0a0534 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -0,0 +1,1067 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.analytics; + +import android.os.SystemClock; +import android.util.Pair; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; +import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; + +/** + * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player. + * + *

    For accurate measurements, the listener should be added to the player before loading media, + * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}. + * + *

    Playback stats are gathered separately for each playback session, i.e. each window in the + * {@link Timeline} and each single ad. + */ +public final class PlaybackStatsListener + implements AnalyticsListener, PlaybackSessionManager.Listener { + + /** A listener for {@link PlaybackStats} updates. */ + public interface Callback { + + /** + * Called when a playback session ends and its {@link PlaybackStats} are ready. + * + * @param eventTime The {@link EventTime} at which the playback session started. Can be used to + * identify the playback session. + * @param playbackStats The {@link PlaybackStats} for the ended playback session. + */ + void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats); + } + + private final PlaybackSessionManager sessionManager; + private final Map playbackStatsTrackers; + private final Map sessionStartEventTimes; + @Nullable private final Callback callback; + private final boolean keepHistory; + private final Period period; + + private PlaybackStats finishedPlaybackStats; + @Nullable private String activeContentPlayback; + @Nullable private String activeAdPlayback; + private boolean playWhenReady; + @Player.State private int playbackState; + private boolean isSuppressed; + private float playbackSpeed; + + /** + * Creates listener for playback stats. + * + * @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of + * events. + * @param callback An optional callback for finished {@link PlaybackStats}. + */ + public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) { + this.callback = callback; + this.keepHistory = keepHistory; + sessionManager = new DefaultPlaybackSessionManager(); + playbackStatsTrackers = new HashMap<>(); + sessionStartEventTimes = new HashMap<>(); + finishedPlaybackStats = PlaybackStats.EMPTY; + playWhenReady = false; + playbackState = Player.STATE_IDLE; + playbackSpeed = 1f; + period = new Period(); + sessionManager.setListener(this); + } + + /** + * Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is + * listening to. + * + *

    Note that these {@link PlaybackStats} will not contain the full history of events. + * + * @return The combined {@link PlaybackStats} for all playback sessions. + */ + public PlaybackStats getCombinedPlaybackStats() { + PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1]; + allPendingPlaybackStats[0] = finishedPlaybackStats; + int index = 1; + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false); + } + return PlaybackStats.merge(allPendingPlaybackStats); + } + + /** + * Returns the {@link PlaybackStats} for the currently playback session, or null if no session is + * active. + * + * @return {@link PlaybackStats} for the current playback session. + */ + @Nullable + public PlaybackStats getPlaybackStats() { + PlaybackStatsTracker activeStatsTracker = + activeAdPlayback != null + ? playbackStatsTrackers.get(activeAdPlayback) + : activeContentPlayback != null + ? playbackStatsTrackers.get(activeContentPlayback) + : null; + return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false); + } + + /** + * Finishes all pending playback sessions. Should be called when the listener is removed from the + * player or when the player is released. + */ + public void finishAllSessions() { + // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with + // an actual EventTime. Should also simplify other cases where the listener needs to be released + // separately from the player. + EventTime dummyEventTime = + new EventTime( + SystemClock.elapsedRealtime(), + Timeline.EMPTY, + /* windowIndex= */ 0, + /* mediaPeriodId= */ null, + /* eventPlaybackPositionMs= */ 0, + /* currentPlaybackPositionMs= */ 0, + /* totalBufferedDurationMs= */ 0); + sessionManager.finishAllSessions(dummyEventTime); + } + + // PlaybackSessionManager.Listener implementation. + + @Override + public void onSessionCreated(EventTime eventTime, String session) { + PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); + tracker.onPlayerStateChanged( + eventTime, playWhenReady, playbackState, /* belongsToPlayback= */ true); + tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true); + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); + playbackStatsTrackers.put(session, tracker); + sessionStartEventTimes.put(session, eventTime); + } + + @Override + public void onSessionActive(EventTime eventTime, String session) { + Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime); + if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) { + activeAdPlayback = session; + } else { + activeContentPlayback = session; + } + } + + @Override + public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) { + Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd()); + long contentPeriodPositionUs = + eventTime + .timeline + .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period) + .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex); + long contentWindowPositionUs = + contentPeriodPositionUs == C.TIME_END_OF_SOURCE + ? C.TIME_END_OF_SOURCE + : contentPeriodPositionUs + period.getPositionInWindowUs(); + EventTime contentEventTime = + new EventTime( + eventTime.realtimeMs, + eventTime.timeline, + eventTime.windowIndex, + new MediaPeriodId( + eventTime.mediaPeriodId.periodUid, + eventTime.mediaPeriodId.windowSequenceNumber, + eventTime.mediaPeriodId.adGroupIndex), + /* eventPlaybackPositionMs= */ C.usToMs(contentWindowPositionUs), + eventTime.currentPlaybackPositionMs, + eventTime.totalBufferedDurationMs); + Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) + .onInterruptedByAd(contentEventTime); + } + + @Override + public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) { + if (session.equals(activeAdPlayback)) { + activeAdPlayback = null; + } else if (session.equals(activeContentPlayback)) { + activeContentPlayback = null; + } + PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session)); + EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session)); + if (automaticTransition) { + // Simulate ENDED state to record natural ending of playback. + tracker.onPlayerStateChanged( + eventTime, /* playWhenReady= */ true, Player.STATE_ENDED, /* belongsToPlayback= */ false); + } + tracker.onFinished(eventTime); + PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); + finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats); + if (callback != null) { + callback.onPlaybackStatsReady(startEventTime, playbackStats); + } + } + + // AnalyticsListener implementation. + + @Override + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, @Player.State int playbackState) { + this.playWhenReady = playWhenReady; + this.playbackState = playbackState; + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers + .get(session) + .onPlayerStateChanged(eventTime, playWhenReady, playbackState, belongsToPlayback); + } + } + + @Override + public void onPlaybackSuppressionReasonChanged( + EventTime eventTime, int playbackSuppressionReason) { + isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE; + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); + playbackStatsTrackers + .get(session) + .onIsSuppressedChanged(eventTime, isSuppressed, belongsToPlayback); + } + } + + @Override + public void onTimelineChanged(EventTime eventTime, int reason) { + sessionManager.handleTimelineUpdate(eventTime); + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + } + } + } + + @Override + public void onPositionDiscontinuity(EventTime eventTime, int reason) { + sessionManager.handlePositionDiscontinuity(eventTime, reason); + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime); + } + } + } + + @Override + public void onSeekStarted(EventTime eventTime) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onSeekStarted(eventTime); + } + } + } + + @Override + public void onSeekProcessed(EventTime eventTime) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onSeekProcessed(eventTime); + } + } + } + + @Override + public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onFatalError(eventTime, error); + } + } + } + + @Override + public void onPlaybackParametersChanged( + EventTime eventTime, PlaybackParameters playbackParameters) { + playbackSpeed = playbackParameters.speed; + maybeAddSession(eventTime); + for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { + tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); + } + } + + @Override + public void onTracksChanged( + EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections); + } + } + } + + @Override + public void onLoadStarted( + EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onLoadStarted(eventTime); + } + } + } + + @Override + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData); + } + } + } + + @Override + public void onVideoSizeChanged( + EventTime eventTime, + int width, + int height, + int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height); + } + } + } + + @Override + public void onBandwidthEstimate( + EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded); + } + } + } + + @Override + public void onAudioUnderrun( + EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onAudioUnderrun(); + } + } + } + + @Override + public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames); + } + } + } + + @Override + public void onLoadError( + EventTime eventTime, + LoadEventInfo loadEventInfo, + MediaLoadData mediaLoadData, + IOException error, + boolean wasCanceled) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); + } + } + } + + @Override + public void onDrmSessionManagerError(EventTime eventTime, Exception error) { + maybeAddSession(eventTime); + for (String session : playbackStatsTrackers.keySet()) { + if (sessionManager.belongsToSession(eventTime, session)) { + playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); + } + } + } + + private void maybeAddSession(EventTime eventTime) { + boolean isCompletelyIdle = eventTime.timeline.isEmpty() && playbackState == Player.STATE_IDLE; + if (!isCompletelyIdle) { + sessionManager.updateSessions(eventTime); + } + } + + /** Tracker for playback stats of a single playback. */ + private static final class PlaybackStatsTracker { + + // Final stats. + private final boolean keepHistory; + private final long[] playbackStateDurationsMs; + private final List> playbackStateHistory; + private final List mediaTimeHistory; + private final List> videoFormatHistory; + private final List> audioFormatHistory; + private final List> fatalErrorHistory; + private final List> nonFatalErrorHistory; + private final boolean isAd; + + private long firstReportedTimeMs; + private boolean hasBeenReady; + private boolean hasEnded; + private boolean isJoinTimeInvalid; + private int pauseCount; + private int pauseBufferCount; + private int seekCount; + private int rebufferCount; + private long maxRebufferTimeMs; + private int initialVideoFormatHeight; + private long initialVideoFormatBitrate; + private long initialAudioFormatBitrate; + private long videoFormatHeightTimeMs; + private long videoFormatHeightTimeProduct; + private long videoFormatBitrateTimeMs; + private long videoFormatBitrateTimeProduct; + private long audioFormatTimeMs; + private long audioFormatBitrateTimeProduct; + private long bandwidthTimeMs; + private long bandwidthBytes; + private long droppedFrames; + private long audioUnderruns; + private int fatalErrorCount; + private int nonFatalErrorCount; + + // Current player state tracking. + private @PlaybackState int currentPlaybackState; + private long currentPlaybackStateStartTimeMs; + private boolean isSeeking; + private boolean isForeground; + private boolean isInterruptedByAd; + private boolean isFinished; + private boolean playWhenReady; + @Player.State private int playerPlaybackState; + private boolean isSuppressed; + private boolean hasFatalError; + private boolean startedLoading; + private long lastRebufferStartTimeMs; + @Nullable private Format currentVideoFormat; + @Nullable private Format currentAudioFormat; + private long lastVideoFormatStartTimeMs; + private long lastAudioFormatStartTimeMs; + private float currentPlaybackSpeed; + + /** + * Creates a tracker for playback stats. + * + * @param keepHistory Whether to keep a full history of events. + * @param startTime The {@link EventTime} at which the playback stats start. + */ + public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) { + this.keepHistory = keepHistory; + playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT]; + playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); + currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + currentPlaybackStateStartTimeMs = startTime.realtimeMs; + playerPlaybackState = Player.STATE_IDLE; + firstReportedTimeMs = C.TIME_UNSET; + maxRebufferTimeMs = C.TIME_UNSET; + isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd(); + initialAudioFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatBitrate = C.LENGTH_UNSET; + initialVideoFormatHeight = C.LENGTH_UNSET; + currentPlaybackSpeed = 1f; + } + + /** + * Notifies the tracker of a player state change event, including all player state changes while + * the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param playWhenReady Whether the playback will proceed when ready. + * @param playbackState The current {@link Player.State}. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onPlayerStateChanged( + EventTime eventTime, + boolean playWhenReady, + @Player.State int playbackState, + boolean belongsToPlayback) { + this.playWhenReady = playWhenReady; + playerPlaybackState = playbackState; + if (playbackState != Player.STATE_IDLE) { + hasFatalError = false; + } + if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) { + isInterruptedByAd = false; + } + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Notifies the tracker of a change to the playback suppression (e.g. due to audio focus loss), + * including all updates while the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param isSuppressed Whether playback is suppressed. + * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. + */ + public void onIsSuppressedChanged( + EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback) { + this.isSuppressed = isSuppressed; + maybeUpdatePlaybackState(eventTime, belongsToPlayback); + } + + /** + * Notifies the tracker of a position discontinuity or timeline update for the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onPositionDiscontinuity(EventTime eventTime) { + isInterruptedByAd = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of the start of a seek in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onSeekStarted(EventTime eventTime) { + isSeeking = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of a seek has been processed in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onSeekProcessed(EventTime eventTime) { + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker of fatal player error in the current playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onFatalError(EventTime eventTime, Exception error) { + fatalErrorCount++; + if (keepHistory) { + fatalErrorHistory.add(Pair.create(eventTime, error)); + } + hasFatalError = true; + isInterruptedByAd = false; + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that a load for the current playback has started. + * + * @param eventTime The {@link EventTime}. + */ + public void onLoadStarted(EventTime eventTime) { + startedLoading = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback became the active foreground playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onForeground(EventTime eventTime) { + isForeground = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback has been interrupted for ad playback. + * + * @param eventTime The {@link EventTime}. + */ + public void onInterruptedByAd(EventTime eventTime) { + isInterruptedByAd = true; + isSeeking = false; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); + } + + /** + * Notifies the tracker that the current playback has finished. + * + * @param eventTime The {@link EventTime}. Not guaranteed to belong to the current playback. + */ + public void onFinished(EventTime eventTime) { + isFinished = true; + maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false); + } + + /** + * Notifies the tracker that the track selection for the current playback changed. + * + * @param eventTime The {@link EventTime}. + * @param trackSelections The new {@link TrackSelectionArray}. + */ + public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) { + boolean videoEnabled = false; + boolean audioEnabled = false; + for (TrackSelection trackSelection : trackSelections.getAll()) { + if (trackSelection != null && trackSelection.length() > 0) { + int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType); + if (trackType == C.TRACK_TYPE_VIDEO) { + videoEnabled = true; + } else if (trackType == C.TRACK_TYPE_AUDIO) { + audioEnabled = true; + } + } + } + if (!videoEnabled) { + maybeUpdateVideoFormat(eventTime, /* newFormat= */ null); + } + if (!audioEnabled) { + maybeUpdateAudioFormat(eventTime, /* newFormat= */ null); + } + } + + /** + * Notifies the tracker that a format being read by the renderers for the current playback + * changed. + * + * @param eventTime The {@link EventTime}. + * @param mediaLoadData The {@link MediaLoadData} describing the format change. + */ + public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { + if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO + || mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) { + maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat); + } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) { + maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat); + } + } + + /** + * Notifies the tracker that the video size for the current playback changed. + * + * @param eventTime The {@link EventTime}. + * @param width The video width in pixels. + * @param height The video height in pixels. + */ + public void onVideoSizeChanged(EventTime eventTime, int width, int height) { + if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) { + Format formatWithHeight = currentVideoFormat.copyWithVideoSize(width, height); + maybeUpdateVideoFormat(eventTime, formatWithHeight); + } + } + + /** + * Notifies the tracker of a playback speed change, including all playback speed changes while + * the playback is not in the foreground. + * + * @param eventTime The {@link EventTime}. + * @param playbackSpeed The new playback speed. + */ + public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { + maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs); + maybeRecordVideoFormatTime(eventTime.realtimeMs); + maybeRecordAudioFormatTime(eventTime.realtimeMs); + currentPlaybackSpeed = playbackSpeed; + } + + /** Notifies the builder of an audio underrun for the current playback. */ + public void onAudioUnderrun() { + audioUnderruns++; + } + + /** + * Notifies the tracker of dropped video frames for the current playback. + * + * @param droppedFrames The number of dropped video frames. + */ + public void onDroppedVideoFrames(int droppedFrames) { + this.droppedFrames += droppedFrames; + } + + /** + * Notifies the tracker of bandwidth measurement data for the current playback. + * + * @param timeMs The time for which bandwidth measurement data is available, in milliseconds. + * @param bytes The bytes transferred during {@code timeMs}. + */ + public void onBandwidthData(long timeMs, long bytes) { + bandwidthTimeMs += timeMs; + bandwidthBytes += bytes; + } + + /** + * Notifies the tracker of a non-fatal error in the current playback. + * + * @param eventTime The {@link EventTime}. + * @param error The error. + */ + public void onNonFatalError(EventTime eventTime, Exception error) { + nonFatalErrorCount++; + if (keepHistory) { + nonFatalErrorHistory.add(Pair.create(eventTime, error)); + } + } + + /** + * Builds the playback stats. + * + * @param isFinal Whether this is the final build and no further events are expected. + */ + public PlaybackStats build(boolean isFinal) { + long[] playbackStateDurationsMs = this.playbackStateDurationsMs; + List mediaTimeHistory = this.mediaTimeHistory; + if (!isFinal) { + long buildTimeMs = SystemClock.elapsedRealtime(); + playbackStateDurationsMs = + Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT); + long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs); + playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs; + maybeUpdateMaxRebufferTimeMs(buildTimeMs); + maybeRecordVideoFormatTime(buildTimeMs); + maybeRecordAudioFormatTime(buildTimeMs); + mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory); + if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) { + mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs)); + } + } + boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady; + long validJoinTimeMs = + isJoinTimeInvalid + ? C.TIME_UNSET + : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND]; + boolean hasBackgroundJoin = + playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0; + List> videoHistory = + isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory); + List> audioHistory = + isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory); + return new PlaybackStats( + /* playbackCount= */ 1, + playbackStateDurationsMs, + isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), + mediaTimeHistory, + firstReportedTimeMs, + /* foregroundPlaybackCount= */ isForeground ? 1 : 0, + /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1, + /* endedCount= */ hasEnded ? 1 : 0, + /* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0, + validJoinTimeMs, + /* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1, + pauseCount, + pauseBufferCount, + seekCount, + rebufferCount, + maxRebufferTimeMs, + /* adPlaybackCount= */ isAd ? 1 : 0, + videoHistory, + audioHistory, + videoFormatHeightTimeMs, + videoFormatHeightTimeProduct, + videoFormatBitrateTimeMs, + videoFormatBitrateTimeProduct, + audioFormatTimeMs, + audioFormatBitrateTimeProduct, + /* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1, + /* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1, + initialVideoFormatHeight, + initialVideoFormatBitrate, + /* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1, + initialAudioFormatBitrate, + bandwidthTimeMs, + bandwidthBytes, + droppedFrames, + audioUnderruns, + /* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0, + fatalErrorCount, + nonFatalErrorCount, + fatalErrorHistory, + nonFatalErrorHistory); + } + + private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) { + @PlaybackState int newPlaybackState = resolveNewPlaybackState(); + if (newPlaybackState == currentPlaybackState) { + return; + } + Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs); + + long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs; + playbackStateDurationsMs[currentPlaybackState] += stateDurationMs; + if (firstReportedTimeMs == C.TIME_UNSET) { + firstReportedTimeMs = eventTime.realtimeMs; + } + isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState); + hasBeenReady |= isReadyState(newPlaybackState); + hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED; + if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) { + pauseCount++; + } + if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) { + seekCount++; + } + if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) { + rebufferCount++; + lastRebufferStartTimeMs = eventTime.realtimeMs; + } + if (isRebufferingState(currentPlaybackState) + && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + && newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) { + pauseBufferCount++; + } + + maybeUpdateMediaTimeHistory( + eventTime.realtimeMs, + /* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET); + maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs); + maybeRecordVideoFormatTime(eventTime.realtimeMs); + maybeRecordAudioFormatTime(eventTime.realtimeMs); + + currentPlaybackState = newPlaybackState; + currentPlaybackStateStartTimeMs = eventTime.realtimeMs; + if (keepHistory) { + playbackStateHistory.add(Pair.create(eventTime, currentPlaybackState)); + } + } + + private @PlaybackState int resolveNewPlaybackState() { + if (isFinished) { + // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item). + return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED + ? PlaybackStats.PLAYBACK_STATE_ENDED + : PlaybackStats.PLAYBACK_STATE_ABANDONED; + } else if (isSeeking) { + // Seeking takes precedence over errors such that we report a seek while in error state. + return PlaybackStats.PLAYBACK_STATE_SEEKING; + } else if (hasFatalError) { + return PlaybackStats.PLAYBACK_STATE_FAILED; + } else if (!isForeground) { + // Before the playback becomes foreground, only report background joining and not started. + return startedLoading + ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + : PlaybackStats.PLAYBACK_STATE_NOT_STARTED; + } else if (isInterruptedByAd) { + return PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD; + } else if (playerPlaybackState == Player.STATE_ENDED) { + return PlaybackStats.PLAYBACK_STATE_ENDED; + } else if (playerPlaybackState == Player.STATE_BUFFERING) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { + return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND; + } + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING + || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING) { + return PlaybackStats.PLAYBACK_STATE_SEEK_BUFFERING; + } + if (!playWhenReady) { + return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + return isSuppressed + ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING + : PlaybackStats.PLAYBACK_STATE_BUFFERING; + } else if (playerPlaybackState == Player.STATE_READY) { + if (!playWhenReady) { + return PlaybackStats.PLAYBACK_STATE_PAUSED; + } + return isSuppressed + ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED + : PlaybackStats.PLAYBACK_STATE_PLAYING; + } else if (playerPlaybackState == Player.STATE_IDLE + && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) { + // This case only applies for calls to player.stop(). All other IDLE cases are handled by + // !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored. + return PlaybackStats.PLAYBACK_STATE_STOPPED; + } + return currentPlaybackState; + } + + private void maybeUpdateMaxRebufferTimeMs(long nowMs) { + if (isRebufferingState(currentPlaybackState)) { + long rebufferDurationMs = nowMs - lastRebufferStartTimeMs; + if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) { + maxRebufferTimeMs = rebufferDurationMs; + } + } + } + + private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { + if (!keepHistory) { + return; + } + if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { + if (mediaTimeMs == C.TIME_UNSET) { + return; + } + if (!mediaTimeHistory.isEmpty()) { + long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; + if (previousMediaTimeMs != mediaTimeMs) { + mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs}); + } + } + } + mediaTimeHistory.add( + mediaTimeMs == C.TIME_UNSET + ? guessMediaTimeBasedOnElapsedRealtime(realtimeMs) + : new long[] {realtimeMs, mediaTimeMs}); + } + + private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) { + long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1); + long previousRealtimeMs = previousKnownMediaTimeHistory[0]; + long previousMediaTimeMs = previousKnownMediaTimeHistory[1]; + long elapsedMediaTimeEstimateMs = + (long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed); + long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs; + return new long[] {realtimeMs, mediaTimeEstimateMs}; + } + + private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) { + if (Util.areEqual(currentVideoFormat, newFormat)) { + return; + } + maybeRecordVideoFormatTime(eventTime.realtimeMs); + if (newFormat != null) { + if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) { + initialVideoFormatHeight = newFormat.height; + } + if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) { + initialVideoFormatBitrate = newFormat.bitrate; + } + } + currentVideoFormat = newFormat; + if (keepHistory) { + videoFormatHistory.add(Pair.create(eventTime, currentVideoFormat)); + } + } + + private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) { + if (Util.areEqual(currentAudioFormat, newFormat)) { + return; + } + maybeRecordAudioFormatTime(eventTime.realtimeMs); + if (newFormat != null + && initialAudioFormatBitrate == C.LENGTH_UNSET + && newFormat.bitrate != Format.NO_VALUE) { + initialAudioFormatBitrate = newFormat.bitrate; + } + currentAudioFormat = newFormat; + if (keepHistory) { + audioFormatHistory.add(Pair.create(eventTime, currentAudioFormat)); + } + } + + private void maybeRecordVideoFormatTime(long nowMs) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING + && currentVideoFormat != null) { + long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed); + if (currentVideoFormat.height != Format.NO_VALUE) { + videoFormatHeightTimeMs += mediaDurationMs; + videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height; + } + if (currentVideoFormat.bitrate != Format.NO_VALUE) { + videoFormatBitrateTimeMs += mediaDurationMs; + videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate; + } + } + lastVideoFormatStartTimeMs = nowMs; + } + + private void maybeRecordAudioFormatTime(long nowMs) { + if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING + && currentAudioFormat != null + && currentAudioFormat.bitrate != Format.NO_VALUE) { + long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed); + audioFormatTimeMs += mediaDurationMs; + audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate; + } + lastAudioFormatStartTimeMs = nowMs; + } + + private static boolean isReadyState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_PLAYING + || state == PlaybackStats.PLAYBACK_STATE_PAUSED + || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED; + } + + private static boolean isPausedState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_PAUSED + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; + } + + private static boolean isRebufferingState(@PlaybackState int state) { + return state == PlaybackStats.PLAYBACK_STATE_BUFFERING + || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING + || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING; + } + + private static boolean isInvalidJoinTransition( + @PlaybackState int oldState, @PlaybackState int newState) { + if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + && oldState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { + return false; + } + return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND + && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND + && newState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD + && newState != PlaybackStats.PLAYBACK_STATE_PLAYING + && newState != PlaybackStats.PLAYBACK_STATE_PAUSED + && newState != PlaybackStats.PLAYBACK_STATE_SUPPRESSED + && newState != PlaybackStats.PLAYBACK_STATE_ENDED; + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/package-info.java new file mode 100644 index 000000000..2764120d2 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.analytics; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index 4e4964e81..53803ada4 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -31,7 +31,7 @@ import java.nio.ByteBuffer; /** * Utility methods for parsing Dolby TrueHD and (E-)AC-3 syncframes. (E-)AC-3 parsing follows the - * definition in ETSI TS 102 366 V1.2.1. + * definition in ETSI TS 102 366 V1.4.1. */ public final class Ac3Util { @@ -39,8 +39,8 @@ public final class Ac3Util { public static final class SyncFrameInfo { /** - * AC3 stream types. See also ETSI TS 102 366 E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, - * {@link #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. + * AC3 stream types. See also E.1.3.1.1. One of {@link #STREAM_TYPE_UNDEFINED}, {@link + * #STREAM_TYPE_TYPE0}, {@link #STREAM_TYPE_TYPE1} or {@link #STREAM_TYPE_TYPE2}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -114,9 +114,7 @@ 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. - */ + /** Each syncframe has 6 blocks that provide 256 new audio samples. See subsection 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. @@ -134,20 +132,21 @@ public final class Ac3Util { * 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}; + /** Nominal bitrates in kbps, indexed by frmsizecod / 2. (See 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 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. + * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to 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. @@ -156,7 +155,7 @@ public final class Ac3Util { * @return The AC-3 format parsed from data in the header. */ public static Format parseAc3AnnexFFormat( - ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { int fscod = (data.readUnsignedByte() & 0xC0) >> 6; int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; int nextByte = data.readUnsignedByte(); @@ -179,8 +178,8 @@ public final class Ac3Util { } /** - * 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. + * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to 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. @@ -189,7 +188,7 @@ public final class Ac3Util { * @return The E-AC-3 format parsed from data in the header. */ public static Format parseEAc3AnnexFFormat( - ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { data.skipBytes(2); // data_rate, num_ind_sub // Read the first independent substream. @@ -243,9 +242,10 @@ public final class Ac3Util { public static SyncFrameInfo parseAc3SyncframeInfo(ParsableBitArray data) { int initialPosition = data.getPosition(); data.skipBits(40); - boolean isEac3 = data.readBits(5) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = data.readBits(5) > 10; data.setPosition(initialPosition); - String mimeType; + @Nullable String mimeType; @StreamType int streamType = SyncFrameInfo.STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; @@ -254,7 +254,7 @@ public final class Ac3Util { boolean lfeon; int channelCount; if (isEac3) { - // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. + // Subsection E.1.2. data.skipBits(16); // syncword switch (data.readBits(2)) { // strmtyp case 0: @@ -472,7 +472,8 @@ public final class Ac3Util { if (data.length < 6) { return C.LENGTH_UNSET; } - boolean isEac3 = ((data[5] & 0xFF) >> 3) == 16; // See bsid in subsection E.1.3.1.6. + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((data[5] & 0xF8) >> 3) > 10; if (isEac3) { int frmsiz = (data[2] & 0x07) << 8; // Most significant 3 bits. frmsiz |= data[3] & 0xFF; // Least significant 8 bits. @@ -485,24 +486,22 @@ public final class Ac3Util { } /** - * Returns the number of audio samples in an AC-3 syncframe. - */ - public static int getAc3SyncframeAudioSampleCount() { - return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; - } - - /** - * Reads the number of audio samples represented by the given E-AC-3 syncframe. The buffer's + * Reads the number of audio samples represented by the given (E-)AC-3 syncframe. The buffer's * position is not modified. * * @param buffer The {@link ByteBuffer} from which to read the syncframe. * @return The number of audio samples represented by the syncframe. */ - 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]); + public static int parseAc3SyncframeAudioSampleCount(ByteBuffer buffer) { + // Parse the bitstream ID for AC-3 and E-AC-3 (see subsections 4.3, E.1.2 and E.1.3.1.6). + boolean isEac3 = ((buffer.get(buffer.position() + 5) & 0xF8) >> 3) > 10; + if (isEac3) { + int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; + int numblkscod = fscod == 0x03 ? 3 : (buffer.get(buffer.position() + 4) & 0x30) >> 4; + return BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[numblkscod] * AUDIO_SAMPLES_PER_AUDIO_BLOCK; + } else { + return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + } } /** @@ -535,8 +534,8 @@ public final class Ac3Util { * contain the start of a syncframe. */ public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { - // TODO: Link to specification if available. - // The syncword ends 0xBA for TrueHD or 0xBB for MLP. + // See "Dolby TrueHD (MLP) high-level bitstream description" on the Dolby developer site, + // subsections 2.2 and 4.2.1. The syncword ends 0xBA for TrueHD or 0xBB for MLP. if (syncframe[4] != (byte) 0xF8 || syncframe[5] != (byte) 0x72 || syncframe[6] != (byte) 0x6F diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java index 74bd5bfe9..b9f1dc546 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/Ac4Util.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.audio; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; @@ -56,6 +57,11 @@ public final class Ac4Util { /** The channel count of AC-4 stream. */ // TODO: Parse AC-4 stream channel count. private static final int CHANNEL_COUNT_2 = 2; + /** + * The AC-4 sync frame header size for extractor. The seven bytes are 0xAC, 0x40, 0xFF, 0xFF, + * sizeByte1, sizeByte2, sizeByte3. See ETSI TS 103 190-1 V1.3.1, Annex G + */ + public static final int SAMPLE_HEADER_SIZE = 7; /** * The header size for AC-4 parser. Only needs to be as big as we need to read, not the full * header size. @@ -95,7 +101,7 @@ public final class Ac4Util { * @return The AC-4 format parsed from data in the header. */ public static Format parseAc4AnnexEFormat( - ParsableByteArray data, String trackId, String language, DrmInitData drmInitData) { + ParsableByteArray data, String trackId, String language, @Nullable DrmInitData drmInitData) { data.skipBytes(1); // ac4_dsi_version, bitstream_version[0:5] int sampleRate = ((data.readUnsignedByte() & 0x20) >> 5 == 1) ? 48000 : 44100; return Format.createAudioSampleFormat( @@ -217,7 +223,7 @@ public final class Ac4Util { /** Populates {@code buffer} with an AC-4 sample header for a sample of the specified size. */ public static void getAc4SampleHeader(int size, ParsableByteArray buffer) { // See ETSI TS 103 190-1 V1.3.1, Annex G. - buffer.reset(/* limit= */ 7); + buffer.reset(SAMPLE_HEADER_SIZE); buffer.data[0] = (byte) 0xAC; buffer.data[1] = 0x40; buffer.data[2] = (byte) 0xFF; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java index 9c63eb42c..516df8147 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioAttributes.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.audio; import android.annotation.TargetApi; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; /** * Attributes for audio playback, which configure the underlying platform @@ -42,17 +43,19 @@ public final class AudioAttributes { private @C.AudioContentType int contentType; private @C.AudioFlags int flags; private @C.AudioUsage int usage; + private @C.AudioAllowedCapturePolicy int allowedCapturePolicy; /** * Creates a new builder for {@link AudioAttributes}. - *

    - * By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is - * {@link C#USAGE_MEDIA}, and no flags are set. + * + *

    By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is {@link + * C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set. */ public Builder() { contentType = C.CONTENT_TYPE_UNKNOWN; flags = 0; usage = C.USAGE_MEDIA; + allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL; } /** @@ -79,11 +82,15 @@ public final class AudioAttributes { return this; } - /** - * Creates an {@link AudioAttributes} instance from this builder. - */ + /** See {@link android.media.AudioAttributes.Builder#setAllowedCapturePolicy(int)}. */ + public Builder setAllowedCapturePolicy(@C.AudioAllowedCapturePolicy int allowedCapturePolicy) { + this.allowedCapturePolicy = allowedCapturePolicy; + return this; + } + + /** Creates an {@link AudioAttributes} instance from this builder. */ public AudioAttributes build() { - return new AudioAttributes(contentType, flags, usage); + return new AudioAttributes(contentType, flags, usage, allowedCapturePolicy); } } @@ -91,24 +98,38 @@ public final class AudioAttributes { public final @C.AudioContentType int contentType; public final @C.AudioFlags int flags; public final @C.AudioUsage int usage; + public final @C.AudioAllowedCapturePolicy int allowedCapturePolicy; - private @Nullable android.media.AudioAttributes audioAttributesV21; + @Nullable private android.media.AudioAttributes audioAttributesV21; - private AudioAttributes(@C.AudioContentType int contentType, @C.AudioFlags int flags, - @C.AudioUsage int usage) { + private AudioAttributes( + @C.AudioContentType int contentType, + @C.AudioFlags int flags, + @C.AudioUsage int usage, + @C.AudioAllowedCapturePolicy int allowedCapturePolicy) { this.contentType = contentType; this.flags = flags; this.usage = usage; + this.allowedCapturePolicy = allowedCapturePolicy; } + /** + * Returns a {@link android.media.AudioAttributes} from this instance. + * + *

    Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29. + */ @TargetApi(21) public android.media.AudioAttributes getAudioAttributesV21() { if (audioAttributesV21 == null) { - audioAttributesV21 = new android.media.AudioAttributes.Builder() - .setContentType(contentType) - .setFlags(flags) - .setUsage(usage) - .build(); + android.media.AudioAttributes.Builder builder = + new android.media.AudioAttributes.Builder() + .setContentType(contentType) + .setFlags(flags) + .setUsage(usage); + if (Util.SDK_INT >= 29) { + builder.setAllowedCapturePolicy(allowedCapturePolicy); + } + audioAttributesV21 = builder.build(); } return audioAttributesV21; } @@ -122,8 +143,10 @@ public final class AudioAttributes { return false; } AudioAttributes other = (AudioAttributes) obj; - return this.contentType == other.contentType && this.flags == other.flags - && this.usage == other.usage; + return this.contentType == other.contentType + && this.flags == other.flags + && this.usage == other.usage + && this.allowedCapturePolicy == other.allowedCapturePolicy; } @Override @@ -132,6 +155,7 @@ public final class AudioAttributes { result = 31 * result + contentType; result = 31 * result + flags; result = 31 * result + usage; + result = 31 * result + allowedCapturePolicy; return result; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java index 1bf141cb4..f75b2cd31 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -23,24 +25,56 @@ import java.nio.ByteOrder; * Interface for audio processors, which take audio data as input and transform it, potentially * modifying its channel count, encoding and/or sample rate. * - *

    Call {@link #configure(int, int, int)} to configure the processor to receive input audio, then - * call {@link #isActive()} to determine whether the processor is active in the new configuration. - * {@link #queueInput(ByteBuffer)}, {@link #getOutputChannelCount()}, {@link #getOutputEncoding()} - * and {@link #getOutputSampleRateHz()} may only be called if the processor is active. Call {@link - * #reset()} to reset the processor to its unconfigured state and release any resources. - * *

    In addition to being able to modify the format of audio, implementations may allow parameters * to be set that affect the output audio and whether the processor is active/inactive. */ public interface AudioProcessor { - /** Exception thrown when a processor can't be configured for a given input audio format. */ - final class UnhandledFormatException extends Exception { + /** PCM audio format that may be handled by an audio processor. */ + final class AudioFormat { + public static final AudioFormat NOT_SET = + new AudioFormat( + /* sampleRate= */ Format.NO_VALUE, + /* channelCount= */ Format.NO_VALUE, + /* encoding= */ Format.NO_VALUE); - public UnhandledFormatException( - int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding " - + encoding); + /** The sample rate in Hertz. */ + public final int sampleRate; + /** The number of interleaved channels. */ + public final int channelCount; + /** The type of linear PCM encoding. */ + @C.PcmEncoding public final int encoding; + /** The number of bytes used to represent one audio frame. */ + public final int bytesPerFrame; + + public AudioFormat(int sampleRate, int channelCount, @C.PcmEncoding int encoding) { + this.sampleRate = sampleRate; + this.channelCount = channelCount; + this.encoding = encoding; + bytesPerFrame = + Util.isEncodingLinearPcm(encoding) + ? Util.getPcmFrameSize(encoding, channelCount) + : Format.NO_VALUE; + } + + @Override + public String toString() { + return "AudioFormat[" + + "sampleRate=" + + sampleRate + + ", channelCount=" + + channelCount + + ", encoding=" + + encoding + + ']'; + } + } + + /** Exception thrown when a processor can't be configured for a given input audio format. */ + final class UnhandledAudioFormatException extends Exception { + + public UnhandledAudioFormatException(AudioFormat inputAudioFormat) { + super("Unhandled format: " + inputAudioFormat); } } @@ -50,47 +84,24 @@ public interface AudioProcessor { /** * Configures the processor to process input audio with the specified format. After calling this - * method, call {@link #isActive()} to determine whether the audio processor is active. + * method, call {@link #isActive()} to determine whether the audio processor is active. Returns + * the configured output audio format if this instance is active. * - *

    If the audio processor is active after configuration, call {@link #getOutputSampleRateHz()}, - * {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} to get its new output format. + *

    After calling this method, it is necessary to {@link #flush()} the processor to apply the + * new configuration. Before applying the new configuration, it is safe to queue input and get + * output in the old input/output formats. Call {@link #queueEndOfStream()} when no more input + * will be supplied in the old input format. * - *

    If this method returns {@code true}, it is necessary to {@link #flush()} the processor - * before queueing more data, but you can (optionally) first drain output in the previous - * configuration by calling {@link #queueEndOfStream()} and {@link #getOutput()}. If this method - * returns {@code false}, it is safe to queue new input immediately. - * - * @param sampleRateHz The sample rate of input audio in Hz. - * @param channelCount The number of interleaved channels in input audio. - * @param encoding The encoding of input audio. - * @return Whether the processor must be {@link #flush() flushed} before queueing more input. - * @throws UnhandledFormatException Thrown if the specified format can't be handled as input. + * @param inputAudioFormat The format of audio that will be queued after the next call to {@link + * #flush()}. + * @return The configured output audio format if this instance is {@link #isActive() active}. + * @throws UnhandledAudioFormatException Thrown if the specified format can't be handled as input. */ - boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException; + AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException; /** Returns whether the processor is configured and will process input buffers. */ boolean isActive(); - /** - * Returns the number of audio channels in the data output by the processor. The value may change - * as a result of calling {@link #configure(int, int, int)}. - */ - int getOutputChannelCount(); - - /** - * Returns the audio encoding used in the data output by the processor. The value may change as a - * result of calling {@link #configure(int, int, int)}. - */ - @C.PcmEncoding - int getOutputEncoding(); - - /** - * Returns the sample rate of audio output by the processor, in hertz. The value may change as a - * result of calling {@link #configure(int, int, int)}. - */ - int getOutputSampleRateHz(); - /** * Queues audio data between the position and limit of the input {@code buffer} for processing. * {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as @@ -132,6 +143,6 @@ public interface AudioProcessor { */ void flush(); - /** Resets the processor to its unconfigured state. */ + /** Resets the processor to its unconfigured state, releasing any resources. */ void reset(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 042738b4f..bf5822caf 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.SystemClock; import androidx.annotation.Nullable; @@ -105,8 +107,8 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}. */ public void enabled(final DecoderCounters decoderCounters) { - if (listener != null) { - handler.post(() -> listener.onAudioEnabled(decoderCounters)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioEnabled(decoderCounters)); } } @@ -115,11 +117,12 @@ public interface AudioRendererEventListener { */ public void decoderInitialized(final String decoderName, final long initializedTimestampMs, final long initializationDurationMs) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onAudioDecoderInitialized( - decoderName, initializedTimestampMs, initializationDurationMs)); + castNonNull(listener) + .onAudioDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } @@ -127,8 +130,8 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */ public void inputFormatChanged(final Format format) { - if (listener != null) { - handler.post(() -> listener.onAudioInputFormatChanged(format)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format)); } } @@ -137,9 +140,11 @@ public interface AudioRendererEventListener { */ public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) { - if (listener != null) { + if (handler != null) { handler.post( - () -> listener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); + () -> + castNonNull(listener) + .onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); } } @@ -148,11 +153,11 @@ public interface AudioRendererEventListener { */ public void disabled(final DecoderCounters counters) { counters.ensureUpdated(); - if (listener != null) { + if (handler != null) { handler.post( () -> { counters.ensureUpdated(); - listener.onAudioDisabled(counters); + castNonNull(listener).onAudioDisabled(counters); }); } } @@ -161,11 +166,9 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}. */ public void audioSessionId(final int audioSessionId) { - if (listener != null) { - handler.post(() -> listener.onAudioSessionId(audioSessionId)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onAudioSessionId(audioSessionId)); } } - } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 393380453..f2458a747 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -259,13 +259,12 @@ public interface AudioSink { boolean hasPendingData(); /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. + * Attempts to set the playback parameters. The audio sink may override these parameters if they + * are not supported. * * @param playbackParameters The new playback parameters to attempt to set. - * @return The active playback parameters. */ - PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + void setPlaybackParameters(PlaybackParameters playbackParameters); /** * Gets the active {@link PlaybackParameters}. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java index d43972d7b..200c91795 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioTimestampPoller.java @@ -37,7 +37,7 @@ import java.lang.annotation.RetentionPolicy; * *

    If {@link #hasTimestamp()} returns {@code true}, call {@link #getTimestampSystemTimeUs()} to * get the system time at which the latest timestamp was sampled and {@link - * #getTimestampPositionFrames()} to get its position in frames. If {@link #isTimestampAdvancing()} + * #getTimestampPositionFrames()} to get its position in frames. If {@link #hasAdvancingTimestamp()} * returns {@code true}, the caller should assume that the timestamp has been increasing in real * time since it was sampled. Otherwise, it may be stationary. * @@ -68,7 +68,7 @@ import java.lang.annotation.RetentionPolicy; private static final int STATE_ERROR = 4; /** The polling interval for {@link #STATE_INITIALIZING} and {@link #STATE_TIMESTAMP}. */ - private static final int FAST_POLL_INTERVAL_US = 5_000; + private static final int FAST_POLL_INTERVAL_US = 10_000; /** * The polling interval for {@link #STATE_TIMESTAMP_ADVANCING} and {@link #STATE_NO_TIMESTAMP}. */ @@ -82,7 +82,7 @@ import java.lang.annotation.RetentionPolicy; */ private static final int INITIALIZING_DURATION_US = 500_000; - private final @Nullable AudioTimestampV19 audioTimestamp; + @Nullable private final AudioTimestampV19 audioTimestamp; private @State int state; private long initializeSystemTimeUs; @@ -110,7 +110,7 @@ import java.lang.annotation.RetentionPolicy; * timestamp is available via {@link #getTimestampSystemTimeUs()} and {@link * #getTimestampPositionFrames()}, and the caller should call {@link #acceptTimestamp()} if the * timestamp was valid, or {@link #rejectTimestamp()} otherwise. The values returned by {@link - * #hasTimestamp()} and {@link #isTimestampAdvancing()} may be updated. + * #hasTimestamp()} and {@link #hasAdvancingTimestamp()} may be updated. * * @param systemTimeUs The current system time, in microseconds. * @return Whether the timestamp was updated. @@ -200,12 +200,12 @@ import java.lang.annotation.RetentionPolicy; } /** - * Returns whether the timestamp appears to be advancing. If {@code true}, call {@link + * Returns whether this instance has an advancing timestamp. If {@code true}, call {@link * #getTimestampSystemTimeUs()} and {@link #getTimestampSystemTimeUs()} to access the timestamp. A * current position for the track can be extrapolated based on elapsed real time since the system * time at which the timestamp was sampled. */ - public boolean isTimestampAdvancing() { + public boolean hasAdvancingTimestamp() { return state == STATE_TIMESTAMP_ADVANCING; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index e87e49d2d..d944edc19 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -123,6 +123,8 @@ import java.lang.reflect.Method; *

    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; + /** The duration of time used to smooth over an adjustment between position sampling modes. */ + private static final long MODE_SWITCH_SMOOTHING_DURATION_US = C.MICROS_PER_SECOND; private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200; @@ -133,10 +135,10 @@ import java.lang.reflect.Method; private final Listener listener; private final long[] playheadOffsets; - private @Nullable AudioTrack audioTrack; + @Nullable private AudioTrack audioTrack; private int outputPcmFrameSize; private int bufferSize; - private @Nullable AudioTimestampPoller audioTimestampPoller; + @Nullable private AudioTimestampPoller audioTimestampPoller; private int outputSampleRate; private boolean needsPassthroughWorkarounds; private long bufferSizeUs; @@ -144,7 +146,7 @@ import java.lang.reflect.Method; private long smoothedPlayheadOffsetUs; private long lastPlayheadSampleTimeUs; - private @Nullable Method getLatencyMethod; + @Nullable private Method getLatencyMethod; private long latencyUs; private boolean hasData; @@ -160,6 +162,15 @@ import java.lang.reflect.Method; private long stopPlaybackHeadPosition; private long endPlaybackHeadPosition; + // Results from the previous call to getCurrentPositionUs. + private long lastPositionUs; + private long lastSystemTimeUs; + private boolean lastSampleUsedGetTimestampMode; + + // Results from the last call to getCurrentPositionUs that used a different sample mode. + private long previousModePositionUs; + private long previousModeSystemTimeUs; + /** * Creates a new audio track position tracker. * @@ -206,6 +217,7 @@ import java.lang.reflect.Method; hasData = false; stopTimestampUs = C.TIME_UNSET; forceResetWorkaroundTimeMs = C.TIME_UNSET; + lastLatencySampleTimeUs = 0; latencyUs = 0; } @@ -217,18 +229,16 @@ import java.lang.reflect.Method; // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. // Otherwise, derive a smoothed position by sampling the track's frame position. long systemTimeUs = System.nanoTime() / 1000; + long positionUs; AudioTimestampPoller audioTimestampPoller = Assertions.checkNotNull(this.audioTimestampPoller); - if (audioTimestampPoller.hasTimestamp()) { + boolean useGetTimestampMode = audioTimestampPoller.hasAdvancingTimestamp(); + if (useGetTimestampMode) { // Calculate the speed-adjusted position using the timestamp (which may be in the future). long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames(); long timestampPositionUs = framesToDurationUs(timestampPositionFrames); - if (!audioTimestampPoller.isTimestampAdvancing()) { - return timestampPositionUs; - } long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs(); - return timestampPositionUs + elapsedSinceTimestampUs; + positionUs = timestampPositionUs + elapsedSinceTimestampUs; } else { - long positionUs; if (playheadOffsetCount == 0) { // The AudioTrack has started, but we don't have any samples to compute a smoothed position. positionUs = getPlaybackHeadPositionUs(); @@ -239,10 +249,31 @@ import java.lang.reflect.Method; positionUs = systemTimeUs + smoothedPlayheadOffsetUs; } if (!sourceEnded) { - positionUs -= latencyUs; + positionUs = Math.max(0, positionUs - latencyUs); } - return positionUs; } + + if (lastSampleUsedGetTimestampMode != useGetTimestampMode) { + // We've switched sampling mode. + previousModeSystemTimeUs = lastSystemTimeUs; + previousModePositionUs = lastPositionUs; + } + long elapsedSincePreviousModeUs = systemTimeUs - previousModeSystemTimeUs; + if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) { + // Use a ramp to smooth between the old mode and the new one to avoid introducing a sudden + // jump if the two modes disagree. + long previousModeProjectedPositionUs = previousModePositionUs + elapsedSincePreviousModeUs; + // A ramp consisting of 1000 points distributed over MODE_SWITCH_SMOOTHING_DURATION_US. + long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US; + positionUs *= rampPoint; + positionUs += (1000 - rampPoint) * previousModeProjectedPositionUs; + positionUs /= 1000; + } + + lastSystemTimeUs = systemTimeUs; + lastPositionUs = positionUs; + lastSampleUsedGetTimestampMode = useGetTimestampMode; + return positionUs; } /** Starts position tracking. Must be called immediately before {@link AudioTrack#play()}. */ @@ -353,7 +384,7 @@ import java.lang.reflect.Method; } /** - * Resets the position tracker. Should be called when the audio track previous passed to {@link + * Resets the position tracker. Should be called when the audio track previously passed to {@link * #setAudioTrack(AudioTrack, int, int, int)} is no longer in use. */ public void reset() { @@ -457,6 +488,8 @@ import java.lang.reflect.Method; playheadOffsetCount = 0; nextPlayheadOffsetIndex = 0; lastPlayheadSampleTimeUs = 0; + lastSystemTimeUs = 0; + previousModeSystemTimeUs = 0; } /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java index a3a85bb43..41cb43650 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/BaseAudioProcessor.java @@ -16,24 +16,23 @@ package com.google.android.exoplayer2.audio; import androidx.annotation.CallSuper; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import java.nio.ByteBuffer; import java.nio.ByteOrder; /** * Base class for audio processors that keep an output buffer and an internal buffer that is reused - * whenever input is queued. + * whenever input is queued. Subclasses should override {@link #onConfigure(AudioFormat)} to return + * the output audio format for the processor if it's active. */ public abstract class BaseAudioProcessor implements AudioProcessor { - /** The configured input sample rate, in Hertz, or {@link Format#NO_VALUE} if not configured. */ - protected int sampleRateHz; - /** The configured input channel count, or {@link Format#NO_VALUE} if not configured. */ - protected int channelCount; - /** The configured input encoding, or {@link Format#NO_VALUE} if not configured. */ - @C.PcmEncoding protected int encoding; + /** The current input audio format. */ + protected AudioFormat inputAudioFormat; + /** The current output audio format. */ + protected AudioFormat outputAudioFormat; + private AudioFormat pendingInputAudioFormat; + private AudioFormat pendingOutputAudioFormat; private ByteBuffer buffer; private ByteBuffer outputBuffer; private boolean inputEnded; @@ -41,29 +40,23 @@ public abstract class BaseAudioProcessor implements AudioProcessor { public BaseAudioProcessor() { buffer = EMPTY_BUFFER; outputBuffer = EMPTY_BUFFER; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; - encoding = Format.NO_VALUE; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; + } + + @Override + public final AudioFormat configure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + pendingInputAudioFormat = inputAudioFormat; + pendingOutputAudioFormat = onConfigure(inputAudioFormat); + return isActive() ? pendingOutputAudioFormat : AudioFormat.NOT_SET; } @Override public boolean isActive() { - return sampleRateHz != Format.NO_VALUE; - } - - @Override - public int getOutputChannelCount() { - return channelCount; - } - - @Override - public int getOutputEncoding() { - return encoding; - } - - @Override - public int getOutputSampleRateHz() { - return sampleRateHz; + return pendingOutputAudioFormat != AudioFormat.NOT_SET; } @Override @@ -91,6 +84,8 @@ public abstract class BaseAudioProcessor implements AudioProcessor { public final void flush() { outputBuffer = EMPTY_BUFFER; inputEnded = false; + inputAudioFormat = pendingInputAudioFormat; + outputAudioFormat = pendingOutputAudioFormat; onFlush(); } @@ -98,26 +93,13 @@ public abstract class BaseAudioProcessor implements AudioProcessor { public final void reset() { flush(); buffer = EMPTY_BUFFER; - sampleRateHz = Format.NO_VALUE; - channelCount = Format.NO_VALUE; - encoding = Format.NO_VALUE; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; onReset(); } - /** Sets the input format of this processor, returning whether the input format has changed. */ - protected final boolean setInputFormat( - int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - if (sampleRateHz == this.sampleRateHz - && channelCount == this.channelCount - && encoding == this.encoding) { - return false; - } - this.sampleRateHz = sampleRateHz; - this.channelCount = channelCount; - this.encoding = encoding; - return true; - } - /** * Replaces the current output buffer with a buffer of at least {@code count} bytes and returns * it. Callers should write to the returned buffer then {@link ByteBuffer#flip()} it so it can be @@ -138,6 +120,12 @@ public abstract class BaseAudioProcessor implements AudioProcessor { return outputBuffer.hasRemaining(); } + /** Called when the processor is configured for a new input format. */ + protected AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + return AudioFormat.NOT_SET; + } + /** Called when the end-of-stream is queued to the processor. */ protected void onQueueEndOfStream() { // Do nothing. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index ea155323b..4fb6af1af 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -19,22 +19,20 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; -import java.util.Arrays; /** * An {@link AudioProcessor} that applies a mapping from input channels onto specified output * channels. This can be used to reorder, duplicate or discard channels. */ +@SuppressWarnings("nullness:initialization.fields.uninitialized") /* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor { @Nullable private int[] pendingOutputChannels; - - private boolean active; @Nullable private int[] outputChannels; /** - * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} - * to start using the new channel map. + * Resets the channel mapping. After calling this method, call {@link #configure(AudioFormat)} to + * start using the new channel map. * * @param outputChannels The mapping from input to output channel indices, or {@code null} to * leave the input unchanged. @@ -45,42 +43,28 @@ import java.util.Arrays; } @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { - boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels); - outputChannels = pendingOutputChannels; - - int[] outputChannels = this.outputChannels; + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + @Nullable int[] outputChannels = pendingOutputChannels; if (outputChannels == null) { - active = false; - return outputChannelsChanged; - } - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); - } - if (!outputChannelsChanged && !setInputFormat(sampleRateHz, channelCount, encoding)) { - return false; + return AudioFormat.NOT_SET; } - active = channelCount != outputChannels.length; + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); + } + + boolean active = inputAudioFormat.channelCount != outputChannels.length; for (int i = 0; i < outputChannels.length; i++) { int channelIndex = outputChannels[i]; - if (channelIndex >= channelCount) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + if (channelIndex >= inputAudioFormat.channelCount) { + throw new UnhandledAudioFormatException(inputAudioFormat); } active |= (channelIndex != i); } - return true; - } - - @Override - public boolean isActive() { - return active; - } - - @Override - public int getOutputChannelCount() { - return outputChannels == null ? channelCount : outputChannels.length; + return active + ? new AudioFormat(inputAudioFormat.sampleRate, outputChannels.length, C.ENCODING_PCM_16BIT) + : AudioFormat.NOT_SET; } @Override @@ -88,24 +72,28 @@ import java.util.Arrays; int[] outputChannels = Assertions.checkNotNull(this.outputChannels); int position = inputBuffer.position(); int limit = inputBuffer.limit(); - int frameCount = (limit - position) / (2 * channelCount); - int outputSize = frameCount * outputChannels.length * 2; + int frameCount = (limit - position) / inputAudioFormat.bytesPerFrame; + int outputSize = frameCount * outputAudioFormat.bytesPerFrame; ByteBuffer buffer = replaceOutputBuffer(outputSize); while (position < limit) { for (int channelIndex : outputChannels) { buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex)); } - position += channelCount * 2; + position += inputAudioFormat.bytesPerFrame; } inputBuffer.position(limit); buffer.flip(); } + @Override + protected void onFlush() { + outputChannels = pendingOutputChannels; + } + @Override protected void onReset() { outputChannels = null; pendingOutputChannels = null; - active = false; } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 6635ec40a..32a819bf8 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -27,6 +27,8 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.audio.AudioProcessor.UnhandledAudioFormatException; +import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -118,9 +120,20 @@ public final class DefaultAudioSink implements AudioSink { /** * Creates a new default chain of audio processors, with the user-defined {@code - * audioProcessors} applied before silence skipping and playback parameters. + * audioProcessors} applied before silence skipping and speed adjustment processors. */ public DefaultAudioProcessorChain(AudioProcessor... audioProcessors) { + this(audioProcessors, new SilenceSkippingAudioProcessor(), new SonicAudioProcessor()); + } + + /** + * Creates a new default chain of audio processors, with the user-defined {@code + * audioProcessors} applied before silence skipping and speed adjustment processors. + */ + public DefaultAudioProcessorChain( + AudioProcessor[] audioProcessors, + SilenceSkippingAudioProcessor silenceSkippingAudioProcessor, + SonicAudioProcessor sonicAudioProcessor) { // The passed-in type may be more specialized than AudioProcessor[], so allocate a new array // rather than using Arrays.copyOf. this.audioProcessors = new AudioProcessor[audioProcessors.length + 2]; @@ -130,8 +143,8 @@ public final class DefaultAudioSink implements AudioSink { /* dest= */ this.audioProcessors, /* destPos= */ 0, /* length= */ audioProcessors.length); - silenceSkippingAudioProcessor = new SilenceSkippingAudioProcessor(); - sonicAudioProcessor = new SonicAudioProcessor(); + this.silenceSkippingAudioProcessor = silenceSkippingAudioProcessor; + this.sonicAudioProcessor = sonicAudioProcessor; this.audioProcessors[audioProcessors.length] = silenceSkippingAudioProcessor; this.audioProcessors[audioProcessors.length + 1] = sonicAudioProcessor; } @@ -236,7 +249,7 @@ public final class DefaultAudioSink implements AudioSink { @Nullable private final AudioCapabilities audioCapabilities; private final AudioProcessorChain audioProcessorChain; - private final boolean enableConvertHighResIntPcmToFloat; + private final boolean enableFloatOutput; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final AudioProcessor[] toIntPcmAvailableAudioProcessors; @@ -246,7 +259,7 @@ public final class DefaultAudioSink implements AudioSink { private final ArrayDeque playbackParametersCheckpoints; @Nullable private Listener listener; - /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */ + /** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */ @Nullable private AudioTrack keepSessionIdAudioTrack; @Nullable private Configuration pendingConfiguration; @@ -297,7 +310,7 @@ public final class DefaultAudioSink implements AudioSink { */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { - this(audioCapabilities, audioProcessors, /* enableConvertHighResIntPcmToFloat= */ false); + this(audioCapabilities, audioProcessors, /* enableFloatOutput= */ false); } /** @@ -307,19 +320,16 @@ public final class DefaultAudioSink implements AudioSink { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before * output. May be empty. - * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution - * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer - * audio processing (for example, speed and pitch adjustment) will not be available when float - * output is in use. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, - boolean enableConvertHighResIntPcmToFloat) { - this( - audioCapabilities, - new DefaultAudioProcessorChain(audioProcessors), - enableConvertHighResIntPcmToFloat); + boolean enableFloatOutput) { + this(audioCapabilities, new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput); } /** @@ -330,18 +340,18 @@ public final class DefaultAudioSink implements AudioSink { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback * parameters adjustments. The instance passed in must not be reused in other sinks. - * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution - * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer - * audio processing (for example, speed and pitch adjustment) will not be available when float - * output is in use. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessorChain audioProcessorChain, - boolean enableConvertHighResIntPcmToFloat) { + boolean enableFloatOutput) { this.audioCapabilities = audioCapabilities; this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); - this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat; + this.enableFloatOutput = enableFloatOutput; releasingConditionVariable = new ConditionVariable(true); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); @@ -420,34 +430,34 @@ public final class DefaultAudioSink implements AudioSink { } boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding); - boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + boolean processingEnabled = isInputPcm; int sampleRate = inputSampleRate; int channelCount = inputChannelCount; @C.Encoding int encoding = inputEncoding; - boolean shouldConvertHighResIntPcmToFloat = - enableConvertHighResIntPcmToFloat + boolean useFloatOutput = + enableFloatOutput && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) - && Util.isEncodingHighResolutionIntegerPcm(inputEncoding); + && Util.isEncodingHighResolutionPcm(inputEncoding); AudioProcessor[] availableAudioProcessors = - shouldConvertHighResIntPcmToFloat - ? toFloatPcmAvailableAudioProcessors - : toIntPcmAvailableAudioProcessors; - boolean flushAudioProcessors = false; + useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; if (processingEnabled) { trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); channelMappingAudioProcessor.setChannelMap(outputChannels); + AudioProcessor.AudioFormat outputFormat = + new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding); for (AudioProcessor audioProcessor : availableAudioProcessors) { try { - flushAudioProcessors |= audioProcessor.configure(sampleRate, channelCount, encoding); - } catch (AudioProcessor.UnhandledFormatException e) { + AudioProcessor.AudioFormat nextFormat = audioProcessor.configure(outputFormat); + if (audioProcessor.isActive()) { + outputFormat = nextFormat; + } + } catch (UnhandledAudioFormatException e) { throw new ConfigurationException(e); } - if (audioProcessor.isActive()) { - channelCount = audioProcessor.getOutputChannelCount(); - sampleRate = audioProcessor.getOutputSampleRateHz(); - encoding = audioProcessor.getOutputEncoding(); - } } + sampleRate = outputFormat.sampleRate; + channelCount = outputFormat.channelCount; + encoding = outputFormat.encoding; } int outputChannelConfig = getChannelConfig(channelCount, isInputPcm); @@ -459,7 +469,7 @@ public final class DefaultAudioSink implements AudioSink { isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET; int outputPcmFrameSize = isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET; - boolean canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat; + boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput; Configuration pendingConfiguration = new Configuration( isInputPcm, @@ -473,11 +483,7 @@ public final class DefaultAudioSink implements AudioSink { processingEnabled, canApplyPlaybackParameters, availableAudioProcessors); - // If we have a pending configuration already, we always drain audio processors as the preceding - // configuration may have required it (even if this one doesn't). - boolean drainAudioProcessors = flushAudioProcessors || this.pendingConfiguration != null; - if (isInitialized() - && (!pendingConfiguration.canReuseAudioTrack(configuration) || drainAudioProcessors)) { + if (isInitialized()) { this.pendingConfiguration = pendingConfiguration; } else { configuration = pendingConfiguration; @@ -832,17 +838,12 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { if (configuration != null && !configuration.canApplyPlaybackParameters) { this.playbackParameters = PlaybackParameters.DEFAULT; - return this.playbackParameters; + return; } - PlaybackParameters lastSetPlaybackParameters = - afterDrainPlaybackParameters != null - ? afterDrainPlaybackParameters - : !playbackParametersCheckpoints.isEmpty() - ? playbackParametersCheckpoints.getLast().playbackParameters - : this.playbackParameters; + PlaybackParameters lastSetPlaybackParameters = getPlaybackParameters(); if (!playbackParameters.equals(lastSetPlaybackParameters)) { if (isInitialized()) { // Drain the audio processors so we can determine the frame position at which the new @@ -854,12 +855,16 @@ public final class DefaultAudioSink implements AudioSink { this.playbackParameters = playbackParameters; } } - return this.playbackParameters; } @Override public PlaybackParameters getPlaybackParameters() { - return playbackParameters; + // Mask the already set parameters. + return afterDrainPlaybackParameters != null + ? afterDrainPlaybackParameters + : !playbackParametersCheckpoints.isEmpty() + ? playbackParametersCheckpoints.getLast().playbackParameters + : playbackParameters; } @Override @@ -1149,9 +1154,7 @@ public final class DefaultAudioSink implements AudioSink { case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_8BIT: - case C.ENCODING_PCM_A_LAW: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_MU_LAW: case Format.NO_VALUE: default: throw new IllegalArgumentException(); @@ -1159,22 +1162,26 @@ public final class DefaultAudioSink implements AudioSink { } private static int getFramesPerEncodedSample(@C.Encoding 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 || encoding == C.ENCODING_E_AC3_JOC) { - return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); - } else if (encoding == C.ENCODING_AC4) { - return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); - } else if (encoding == C.ENCODING_DOLBY_TRUEHD) { - int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); - return syncframeOffset == C.INDEX_UNSET - ? 0 - : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) - * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); - } else { - throw new IllegalStateException("Unexpected audio encoding: " + encoding); + switch (encoding) { + case C.ENCODING_MP3: + return MpegAudioHeader.getFrameSampleCount(buffer.get(buffer.position())); + case C.ENCODING_DTS: + case C.ENCODING_DTS_HD: + return DtsUtil.parseDtsAudioSampleCount(buffer); + case C.ENCODING_AC3: + case C.ENCODING_E_AC3: + case C.ENCODING_E_AC3_JOC: + return Ac3Util.parseAc3SyncframeAudioSampleCount(buffer); + case C.ENCODING_AC4: + return Ac4Util.parseAc4SyncframeAudioSampleCount(buffer); + case C.ENCODING_DOLBY_TRUEHD: + int syncframeOffset = Ac3Util.findTrueHdSyncframeOffset(buffer); + return syncframeOffset == C.INDEX_UNSET + ? 0 + : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); + default: + throw new IllegalStateException("Unexpected audio encoding: " + encoding); } } @@ -1226,7 +1233,6 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.setVolume(volume); } - @SuppressWarnings("deprecation") private static void setVolumeInternalV3(AudioTrack audioTrack, float volume) { audioTrack.setStereoVolume(volume, volume); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java index f65dc3fc4..7af9d9f07 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.audio; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.util.MimeTypes; @@ -80,7 +81,7 @@ public final class DtsUtil { * @return The DTS format parsed from data in the header. */ public static Format parseDtsFormat( - byte[] frame, String trackId, String language, DrmInitData drmInitData) { + byte[] frame, String trackId, @Nullable String language, @Nullable DrmInitData drmInitData) { ParsableBitArray frameBits = getNormalizedFrameHeader(frame); frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE int amode = frameBits.readBits(6); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index 2274d53b5..ca6b4f3f1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -16,12 +16,19 @@ package com.google.android.exoplayer2.audio; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; /** - * An {@link AudioProcessor} that converts 24-bit and 32-bit integer PCM audio to 32-bit float PCM - * audio. + * An {@link AudioProcessor} that converts high resolution PCM audio to 32-bit float. The following + * encodings are supported as input: + * + *

      + *
    • {@link C#ENCODING_PCM_24BIT} + *
    • {@link C#ENCODING_PCM_32BIT} + *
    • {@link C#ENCODING_PCM_FLOAT} ({@link #isActive()} will return {@code false}) + *
    */ /* package */ final class FloatResamplingAudioProcessor extends BaseAudioProcessor { @@ -29,50 +36,56 @@ import java.nio.ByteBuffer; private static final double PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR = 1.0 / 0x7FFFFFFF; @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { - if (!Util.isEncodingHighResolutionIntegerPcm(encoding)) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + @C.PcmEncoding int encoding = inputAudioFormat.encoding; + if (!Util.isEncodingHighResolutionPcm(encoding)) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - return setInputFormat(sampleRateHz, channelCount, encoding); - } - - @Override - public boolean isActive() { - return Util.isEncodingHighResolutionIntegerPcm(encoding); - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_FLOAT; + return encoding != C.ENCODING_PCM_FLOAT + ? new AudioFormat( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_FLOAT) + : AudioFormat.NOT_SET; } @Override public void queueInput(ByteBuffer inputBuffer) { - boolean isInput32Bit = encoding == C.ENCODING_PCM_32BIT; int position = inputBuffer.position(); int limit = inputBuffer.limit(); int size = limit - position; - int resampledSize = isInput32Bit ? size : (size / 3) * 4; - ByteBuffer buffer = replaceOutputBuffer(resampledSize); - if (isInput32Bit) { - for (int i = position; i < limit; i += 4) { - int pcm32BitInteger = - (inputBuffer.get(i) & 0xFF) - | ((inputBuffer.get(i + 1) & 0xFF) << 8) - | ((inputBuffer.get(i + 2) & 0xFF) << 16) - | ((inputBuffer.get(i + 3) & 0xFF) << 24); - writePcm32BitFloat(pcm32BitInteger, buffer); - } - } else { - for (int i = position; i < limit; i += 3) { - int pcm32BitInteger = - ((inputBuffer.get(i) & 0xFF) << 8) - | ((inputBuffer.get(i + 1) & 0xFF) << 16) - | ((inputBuffer.get(i + 2) & 0xFF) << 24); - writePcm32BitFloat(pcm32BitInteger, buffer); - } + ByteBuffer buffer; + switch (inputAudioFormat.encoding) { + case C.ENCODING_PCM_24BIT: + buffer = replaceOutputBuffer((size / 3) * 4); + for (int i = position; i < limit; i += 3) { + int pcm32BitInteger = + ((inputBuffer.get(i) & 0xFF) << 8) + | ((inputBuffer.get(i + 1) & 0xFF) << 16) + | ((inputBuffer.get(i + 2) & 0xFF) << 24); + writePcm32BitFloat(pcm32BitInteger, buffer); + } + break; + case C.ENCODING_PCM_32BIT: + buffer = replaceOutputBuffer(size); + for (int i = position; i < limit; i += 4) { + int pcm32BitInteger = + (inputBuffer.get(i) & 0xFF) + | ((inputBuffer.get(i + 1) & 0xFF) << 8) + | ((inputBuffer.get(i + 2) & 0xFF) << 16) + | ((inputBuffer.get(i + 3) & 0xFF) << 24); + writePcm32BitFloat(pcm32BitInteger, buffer); + } + break; + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); } inputBuffer.position(inputBuffer.limit()); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java new file mode 100644 index 000000000..704bd11cc --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ForwardingAudioSink.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.audio; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.PlaybackParameters; +import java.nio.ByteBuffer; + +/** An overridable {@link AudioSink} implementation forwarding all methods to another sink. */ +public class ForwardingAudioSink implements AudioSink { + + private final AudioSink sink; + + public ForwardingAudioSink(AudioSink sink) { + this.sink = sink; + } + + @Override + public void setListener(Listener listener) { + sink.setListener(listener); + } + + @Override + public boolean supportsOutput(int channelCount, int encoding) { + return sink.supportsOutput(channelCount, encoding); + } + + @Override + public long getCurrentPositionUs(boolean sourceEnded) { + return sink.getCurrentPositionUs(sourceEnded); + } + + @Override + public void configure( + int inputEncoding, + int inputChannelCount, + int inputSampleRate, + int specifiedBufferSize, + @Nullable int[] outputChannels, + int trimStartFrames, + int trimEndFrames) + throws ConfigurationException { + sink.configure( + inputEncoding, + inputChannelCount, + inputSampleRate, + specifiedBufferSize, + outputChannels, + trimStartFrames, + trimEndFrames); + } + + @Override + public void play() { + sink.play(); + } + + @Override + public void handleDiscontinuity() { + sink.handleDiscontinuity(); + } + + @Override + public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + throws InitializationException, WriteException { + return sink.handleBuffer(buffer, presentationTimeUs); + } + + @Override + public void playToEndOfStream() throws WriteException { + sink.playToEndOfStream(); + } + + @Override + public boolean isEnded() { + return sink.isEnded(); + } + + @Override + public boolean hasPendingData() { + return sink.hasPendingData(); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + sink.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return sink.getPlaybackParameters(); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + sink.setAudioAttributes(audioAttributes); + } + + @Override + public void setAudioSessionId(int audioSessionId) { + sink.setAudioSessionId(audioSessionId); + } + + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + sink.setAuxEffectInfo(auxEffectInfo); + } + + @Override + public void enableTunnelingV21(int tunnelingAudioSessionId) { + sink.enableTunnelingV21(tunnelingAudioSessionId); + } + + @Override + public void disableTunneling() { + sink.disableTunneling(); + } + + @Override + public void setVolume(float volume) { + sink.setVolume(volume); + } + + @Override + public void pause() { + sink.pause(); + } + + @Override + public void flush() { + sink.flush(); + } + + @Override + public void reset() { + sink.reset(); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f10f45ecf..e8bd5056b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -28,18 +28,21 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; @@ -76,6 +79,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10; private static final String TAG = "MediaCodecAudioRenderer"; + /** + * Custom key used to indicate bits per sample by some decoders on Vivo devices. For example + * OMX.vivo.alac.decoder on the Vivo Z1 Pro. + */ + private static final String VIVO_BITS_PER_SAMPLE_KEY = "v-bits-per-sample"; private final Context context; private final EventDispatcher eventDispatcher; @@ -87,10 +95,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean codecNeedsDiscardChannelsWorkaround; private boolean codecNeedsEosBufferTimestampWorkaround; private android.media.MediaFormat passthroughMediaFormat; - private @C.Encoding int pcmEncoding; - private int channelCount; - private int encoderDelay; - private int encoderPadding; + @Nullable private Format inputFormat; private long currentPositionUs; private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; @@ -101,6 +106,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param context A context. * @param mediaCodecSelector A decoder selector. */ + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector) { this( context, @@ -119,7 +125,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * 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. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -141,6 +152,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -168,7 +180,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements 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. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -203,7 +220,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before * output. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -237,7 +259,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * 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 audioSink The sink to which audio will be output. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -257,6 +284,36 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media audioSink); } + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @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 audioSink The sink to which audio will be output. + */ + @SuppressWarnings("deprecation") + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { + this( + context, + mediaCodecSelector, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + enableDecoderFallback, + eventHandler, + eventListener, + audioSink); + } + /** * @param context A context. * @param mediaCodecSelector A decoder selector. @@ -274,7 +331,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * 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 audioSink The sink to which audio will be output. + * @deprecated Use {@link #MediaCodecAudioRenderer(Context, MediaCodecSelector, boolean, Handler, + * AudioRendererEventListener, AudioSink)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated public MediaCodecAudioRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -300,66 +361,65 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, Format format) + @Capabilities + protected int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData); + boolean supportsFormatDrm = + format.drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); if (supportsFormatDrm && allowPassthrough(format.channelCount, mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; + return RendererCapabilities.create(FORMAT_HANDLED, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.supportsOutput(format.channelCount, format.pcmEncoding)) || !audioSink.supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { // Assume the decoder outputs 16-bit PCM, unless the input is raw. - return FORMAT_UNSUPPORTED_SUBTYPE; - } - boolean requiresSecureDecryption = false; - DrmInitData drmInitData = format.drmInitData; - if (drmInitData != null) { - for (int i = 0; i < drmInitData.schemeDataCount; i++) { - requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; - } + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } List decoderInfos = - mediaCodecSelector.getDecoderInfos( - format.sampleMimeType, requiresSecureDecryption, /* requiresTunnelingDecoder= */ false); + getDecoderInfos(mediaCodecSelector, format, /* requiresSecureDecoder= */ false); if (decoderInfos.isEmpty()) { - return requiresSecureDecryption - && !mediaCodecSelector - .getDecoderInfos( - format.sampleMimeType, - /* requiresSecureDecoder= */ false, - /* requiresTunnelingDecoder= */ false) - .isEmpty() - ? FORMAT_UNSUPPORTED_DRM - : FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } if (!supportsFormatDrm) { - return FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport int adaptiveSupport = isFormatSupported && decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; + @FormatSupport int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override protected List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) throws DecoderQueryException { - if (allowPassthrough(format.channelCount, format.sampleMimeType)) { + @Nullable String mimeType = format.sampleMimeType; + if (mimeType == null) { + return Collections.emptyList(); + } + if (allowPassthrough(format.channelCount, mimeType)) { + @Nullable MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); if (passthroughDecoderInfo != null) { return Collections.singletonList(passthroughDecoderInfo); @@ -367,8 +427,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } List decoderInfos = mediaCodecSelector.getDecoderInfos( - format.sampleMimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); - if (MimeTypes.AUDIO_E_AC3_JOC.equals(format.sampleMimeType)) { + mimeType, requiresSecureDecoder, /* requiresTunnelingDecoder= */ false); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. List decoderInfosWithEac3 = new ArrayList<>(decoderInfos); decoderInfosWithEac3.addAll( @@ -398,7 +459,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto, + @Nullable MediaCrypto crypto, float codecOperatingRate) { codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats()); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); @@ -434,14 +495,37 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } else if (codecInfo.isSeamlessAdaptationSupported( oldFormat, newFormat, /* isNewFormatComplete= */ true)) { return KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION; - } else if (areCodecConfigurationCompatible(oldFormat, newFormat)) { + } else if (canKeepCodecWithFlush(oldFormat, newFormat)) { return KEEP_CODEC_RESULT_YES_WITH_FLUSH; } else { return KEEP_CODEC_RESULT_NO; } } + /** + * Returns whether the codec can be flushed and reused when switching to a new format. Reuse is + * generally possible when the codec would be configured in an identical way after the format + * change (excluding {@link MediaFormat#KEY_MAX_INPUT_SIZE} and configuration that does not come + * from the {@link Format}). + * + * @param oldFormat The first format. + * @param newFormat The second format. + * @return Whether the codec can be flushed and reused when switching to a new format. + */ + protected boolean canKeepCodecWithFlush(Format oldFormat, Format newFormat) { + // Flush and reuse the codec if the audio format and initialization data matches. For Opus, we + // don't flush and reuse the codec because the decoder may discard samples after flushing, which + // would result in audio being dropped just after a stream change (see [Internal: b/143450854]). + return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType) + && oldFormat.channelCount == newFormat.channelCount + && oldFormat.sampleRate == newFormat.sampleRate + && oldFormat.pcmEncoding == newFormat.pcmEncoding + && oldFormat.initializationDataEquals(newFormat) + && !MimeTypes.AUDIO_OPUS.equals(oldFormat.sampleMimeType); + } + @Override + @Nullable public MediaClock getMediaClock() { return this; } @@ -468,39 +552,37 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - super.onInputFormatChanged(newFormat); - eventDispatcher.inputFormatChanged(newFormat); - // 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(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; - channelCount = newFormat.channelCount; - encoderDelay = newFormat.encoderDelay; - encoderPadding = newFormat.encoderPadding; + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); + inputFormat = formatHolder.format; + eventDispatcher.inputFormatChanged(inputFormat); } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) throws ExoPlaybackException { @C.Encoding int encoding; - MediaFormat format; + MediaFormat mediaFormat; if (passthroughMediaFormat != null) { - format = passthroughMediaFormat; + mediaFormat = passthroughMediaFormat; encoding = getPassthroughEncoding( - format.getInteger(MediaFormat.KEY_CHANNEL_COUNT), - format.getString(MediaFormat.KEY_MIME)); + mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT), + mediaFormat.getString(MediaFormat.KEY_MIME)); } else { - format = outputFormat; - encoding = pcmEncoding; + mediaFormat = outputMediaFormat; + if (outputMediaFormat.containsKey(VIVO_BITS_PER_SAMPLE_KEY)) { + encoding = Util.getPcmEncoding(outputMediaFormat.getInteger(VIVO_BITS_PER_SAMPLE_KEY)); + } else { + encoding = getPcmEncoding(inputFormat); + } } - int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); - int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + int channelCount = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); + int sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); int[] channelMap; - if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) { - channelMap = new int[this.channelCount]; - for (int i = 0; i < this.channelCount; i++) { + if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && inputFormat.channelCount < 6) { + channelMap = new int[inputFormat.channelCount]; + for (int i = 0; i < inputFormat.channelCount; i++) { channelMap[i] = i; } } else { @@ -508,10 +590,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, - encoderPadding); + audioSink.configure( + encoding, + channelCount, + sampleRate, + 0, + channelMap, + inputFormat.encoderDelay, + inputFormat.encoderPadding); } catch (AudioSink.ConfigurationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } } @@ -522,7 +611,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @C.Encoding protected int getPassthroughEncoding(int channelCount, String mimeType) { if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { - if (audioSink.supportsOutput(channelCount, C.ENCODING_E_AC3_JOC)) { + // E-AC3 JOC is object-based so the output channel count is arbitrary. + if (audioSink.supportsOutput(/* channelCount= */ Format.NO_VALUE, C.ENCODING_E_AC3_JOC)) { return MimeTypes.getEncoding(MimeTypes.AUDIO_E_AC3_JOC); } // E-AC3 receivers can decode JOC streams, but in 2-D rather than 3-D, so try to fall back. @@ -659,8 +749,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioSink.setPlaybackParameters(playbackParameters); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override @@ -737,7 +827,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return true; } } catch (AudioSink.InitializationException | AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } return false; } @@ -747,7 +838,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat instead. + throw createRendererException(e, inputFormat); } } @@ -776,7 +868,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * will allow possible adaptation to other compatible formats in {@code streamFormats}. * * @param codecInfo A {@link MediaCodecInfo} describing the decoder. - * @param format The format for which the codec is being configured. + * @param format The {@link Format} for which the codec is being configured. * @param streamFormats The possible stream formats. * @return A suitable maximum input size. */ @@ -798,10 +890,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } /** - * Returns a maximum input buffer size for a given format. + * Returns a maximum input buffer size for a given {@link Format}. * * @param codecInfo A {@link MediaCodecInfo} describing the decoder. - * @param format The format. + * @param format The {@link Format}. * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not * be determined. */ @@ -818,34 +910,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return format.maxInputSize; } - /** - * Returns whether two {@link Format}s will cause the same codec to be configured in an identical - * way, excluding {@link MediaFormat#KEY_MAX_INPUT_SIZE} and configuration that does not come from - * the {@link Format}. - * - * @param oldFormat The first format. - * @param newFormat The second format. - * @return Whether the two formats will cause a codec to be configured in an identical way, - * excluding {@link MediaFormat#KEY_MAX_INPUT_SIZE} and configuration that does not come from - * the {@link Format}. - */ - protected boolean areCodecConfigurationCompatible(Format oldFormat, Format newFormat) { - return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType) - && oldFormat.channelCount == newFormat.channelCount - && oldFormat.sampleRate == newFormat.sampleRate - && oldFormat.initializationDataEquals(newFormat); - } - /** * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec} * for decoding the given {@link Format} for playback. * - * @param format The format of the media. + * @param format The {@link Format} of the media. * @param codecMimeType The MIME type handled by the codec. * @param codecMaxInputSize The maximum input size supported by the codec. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. - * @return The framework media format. + * @return The framework {@link MediaFormat}. */ @SuppressLint("InlinedApi") protected MediaFormat getMediaFormat( @@ -927,6 +1001,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media || Util.DEVICE.startsWith("ms01")); } + @C.Encoding + private static int getPcmEncoding(Format format) { + // If the format is anything other than PCM then we assume that the audio decoder will output + // 16-bit PCM. + return MimeTypes.AUDIO_RAW.equals(format.sampleMimeType) + ? format.pcmEncoding + : C.ENCODING_PCM_16BIT; + } + private final class AudioSinkListener implements AudioSink.Listener { @Override diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index d0c057b67..883f5bcb9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -20,29 +20,36 @@ import com.google.android.exoplayer2.Format; import java.nio.ByteBuffer; /** - * An {@link AudioProcessor} that converts 8-bit, 24-bit and 32-bit integer PCM audio to 16-bit - * integer PCM audio. + * An {@link AudioProcessor} that converts different PCM audio encodings to 16-bit integer PCM. The + * following encodings are supported as input: + * + *
      + *
    • {@link C#ENCODING_PCM_8BIT} + *
    • {@link C#ENCODING_PCM_16BIT} ({@link #isActive()} will return {@code false}) + *
    • {@link C#ENCODING_PCM_16BIT_BIG_ENDIAN} + *
    • {@link C#ENCODING_PCM_24BIT} + *
    • {@link C#ENCODING_PCM_32BIT} + *
    • {@link C#ENCODING_PCM_FLOAT} + *
    */ /* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor { @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { - if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT - && encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + @C.PcmEncoding int encoding = inputAudioFormat.encoding; + if (encoding != C.ENCODING_PCM_8BIT + && encoding != C.ENCODING_PCM_16BIT + && encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN + && encoding != C.ENCODING_PCM_24BIT + && encoding != C.ENCODING_PCM_32BIT + && encoding != C.ENCODING_PCM_FLOAT) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - return setInputFormat(sampleRateHz, channelCount, encoding); - } - - @Override - public boolean isActive() { - return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT; - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_16BIT; + return encoding != C.ENCODING_PCM_16BIT + ? new AudioFormat( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT) + : AudioFormat.NOT_SET; } @Override @@ -52,20 +59,21 @@ import java.nio.ByteBuffer; int limit = inputBuffer.limit(); int size = limit - position; int resampledSize; - switch (encoding) { + switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: resampledSize = size * 2; break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + resampledSize = size; + break; case C.ENCODING_PCM_24BIT: resampledSize = (size / 3) * 2; break; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: resampledSize = size / 2; break; case C.ENCODING_PCM_16BIT: - case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -74,32 +82,45 @@ import java.nio.ByteBuffer; // Resample the little endian input and update the input/output buffers. ByteBuffer buffer = replaceOutputBuffer(resampledSize); - switch (encoding) { + switch (inputAudioFormat.encoding) { case C.ENCODING_PCM_8BIT: - // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + // 8 -> 16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. for (int i = position; i < limit; i++) { buffer.put((byte) 0); buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128)); } break; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: + // Big endian to little endian resampling. Swap the byte order. + for (int i = position; i < limit; i += 2) { + buffer.put(inputBuffer.get(i + 1)); + buffer.put(inputBuffer.get(i)); + } + break; case C.ENCODING_PCM_24BIT: - // 24->16 bit resampling. Drop the least significant byte. + // 24 -> 16 bit resampling. Drop the least significant byte. for (int i = position; i < limit; i += 3) { buffer.put(inputBuffer.get(i + 1)); buffer.put(inputBuffer.get(i + 2)); } break; case C.ENCODING_PCM_32BIT: - // 32->16 bit resampling. Drop the two least significant bytes. + // 32 -> 16 bit resampling. Drop the two least significant bytes. for (int i = position; i < limit; i += 4) { buffer.put(inputBuffer.get(i + 2)); buffer.put(inputBuffer.get(i + 3)); } break; - case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: + // 32 bit floating point -> 16 bit resampling. Floating point values are in the range + // [-1.0, 1.0], so need to be scaled by Short.MAX_VALUE. + for (int i = position; i < limit; i += 4) { + short value = (short) (inputBuffer.getFloat(i) * Short.MAX_VALUE); + buffer.put((byte) (value & 0xFF)); + buffer.put((byte) ((value >> 8) & 0xFF)); + } + break; + case C.ENCODING_PCM_16BIT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java index caf8a6165..7ddb49152 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SilenceSkippingAudioProcessor.java @@ -17,11 +17,13 @@ package com.google.android.exoplayer2.audio; import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** * An {@link AudioProcessor} that skips silence in the input stream. Input and output are 16-bit @@ -30,27 +32,20 @@ import java.nio.ByteBuffer; public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { /** - * The minimum duration of audio that must be below {@link #SILENCE_THRESHOLD_LEVEL} to classify - * that part of audio as silent, in microseconds. + * The default value for {@link #SilenceSkippingAudioProcessor(long, long, short) + * minimumSilenceDurationUs}. */ - private static final long MINIMUM_SILENCE_DURATION_US = 150_000; + public static final long DEFAULT_MINIMUM_SILENCE_DURATION_US = 150_000; /** - * The duration of silence by which to extend non-silent sections, in microseconds. The value must - * not exceed {@link #MINIMUM_SILENCE_DURATION_US}. + * The default value for {@link #SilenceSkippingAudioProcessor(long, long, short) + * paddingSilenceUs}. */ - private static final long PADDING_SILENCE_US = 20_000; + public static final long DEFAULT_PADDING_SILENCE_US = 20_000; /** - * The absolute level below which an individual PCM sample is classified as silent. Note: the - * specified value will be rounded so that the threshold check only depends on the more - * significant byte, for efficiency. + * The default value for {@link #SilenceSkippingAudioProcessor(long, long, short) + * silenceThresholdLevel}. */ - private static final short SILENCE_THRESHOLD_LEVEL = 1024; - - /** - * Threshold for classifying an individual PCM sample as silent based on its more significant - * byte. This is {@link #SILENCE_THRESHOLD_LEVEL} divided by 256 with rounding. - */ - private static final byte SILENCE_THRESHOLD_LEVEL_MSB = (SILENCE_THRESHOLD_LEVEL + 128) >> 8; + public static final short DEFAULT_SILENCE_THRESHOLD_LEVEL = 1024; /** Trimming states. */ @Documented @@ -68,8 +63,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { /** State when the input is silent. */ private static final int STATE_SILENT = 2; + private final long minimumSilenceDurationUs; + private final long paddingSilenceUs; + private final short silenceThresholdLevel; private int bytesPerFrame; - private boolean enabled; /** @@ -91,21 +88,44 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { private boolean hasOutputNoise; private long skippedFrames; - /** Creates a new silence trimming audio processor. */ + /** Creates a new silence skipping audio processor. */ public SilenceSkippingAudioProcessor() { + this( + DEFAULT_MINIMUM_SILENCE_DURATION_US, + DEFAULT_PADDING_SILENCE_US, + DEFAULT_SILENCE_THRESHOLD_LEVEL); + } + + /** + * Creates a new silence skipping audio processor. + * + * @param minimumSilenceDurationUs The minimum duration of audio that must be below {@code + * silenceThresholdLevel} to classify that part of audio as silent, in microseconds. + * @param paddingSilenceUs The duration of silence by which to extend non-silent sections, in + * microseconds. The value must not exceed {@code minimumSilenceDurationUs}. + * @param silenceThresholdLevel The absolute level below which an individual PCM sample is + * classified as silent. + */ + public SilenceSkippingAudioProcessor( + long minimumSilenceDurationUs, long paddingSilenceUs, short silenceThresholdLevel) { + Assertions.checkArgument(paddingSilenceUs <= minimumSilenceDurationUs); + this.minimumSilenceDurationUs = minimumSilenceDurationUs; + this.paddingSilenceUs = paddingSilenceUs; + this.silenceThresholdLevel = silenceThresholdLevel; + maybeSilenceBuffer = Util.EMPTY_BYTE_ARRAY; paddingBuffer = Util.EMPTY_BYTE_ARRAY; } /** - * Sets whether to skip silence in the input. Calling this method will discard any data buffered - * within the processor, and may update the value returned by {@link #isActive()}. + * Sets whether to skip silence in the input. This method may only be called after draining data + * through the processor. The value returned by {@link #isActive()} may change, and the processor + * must be {@link #flush() flushed} before queueing more data. * * @param enabled Whether to skip silence in the input. */ public void setEnabled(boolean enabled) { this.enabled = enabled; - flush(); } /** @@ -119,18 +139,17 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { // AudioProcessor implementation. @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - bytesPerFrame = channelCount * 2; - return setInputFormat(sampleRateHz, channelCount, encoding); + return enabled ? inputAudioFormat : AudioFormat.NOT_SET; } @Override public boolean isActive() { - return super.isActive() && enabled; + return enabled; } @Override @@ -165,12 +184,13 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { @Override protected void onFlush() { - if (isActive()) { - int maybeSilenceBufferSize = durationUsToFrames(MINIMUM_SILENCE_DURATION_US) * bytesPerFrame; + if (enabled) { + bytesPerFrame = inputAudioFormat.bytesPerFrame; + int maybeSilenceBufferSize = durationUsToFrames(minimumSilenceDurationUs) * bytesPerFrame; if (maybeSilenceBuffer.length != maybeSilenceBufferSize) { maybeSilenceBuffer = new byte[maybeSilenceBufferSize]; } - paddingSize = durationUsToFrames(PADDING_SILENCE_US) * bytesPerFrame; + paddingSize = durationUsToFrames(paddingSilenceUs) * bytesPerFrame; if (paddingBuffer.length != paddingSize) { paddingBuffer = new byte[paddingSize]; } @@ -317,7 +337,7 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * Returns the number of input frames corresponding to {@code durationUs} microseconds of audio. */ private int durationUsToFrames(long durationUs) { - return (int) ((durationUs * sampleRateHz) / C.MICROS_PER_SECOND); + return (int) ((durationUs * inputAudioFormat.sampleRate) / C.MICROS_PER_SECOND); } /** @@ -325,9 +345,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * classified as a noisy frame, or the limit of the buffer if no such frame exists. */ private int findNoisePosition(ByteBuffer buffer) { + Assertions.checkArgument(buffer.order() == ByteOrder.LITTLE_ENDIAN); // The input is in ByteOrder.nativeOrder(), which is little endian on Android. - for (int i = buffer.position() + 1; i < buffer.limit(); i += 2) { - if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + for (int i = buffer.position(); i < buffer.limit(); i += 2) { + if (Math.abs(buffer.getShort(i)) > silenceThresholdLevel) { // Round to the start of the frame. return bytesPerFrame * (i / bytesPerFrame); } @@ -340,9 +361,10 @@ public final class SilenceSkippingAudioProcessor extends BaseAudioProcessor { * from the byte position to the limit are classified as silent. */ private int findNoiseLimit(ByteBuffer buffer) { + Assertions.checkArgument(buffer.order() == ByteOrder.LITTLE_ENDIAN); // The input is in ByteOrder.nativeOrder(), which is little endian on Android. - for (int i = buffer.limit() - 1; i >= buffer.position(); i -= 2) { - if (Math.abs(buffer.get(i)) > SILENCE_THRESHOLD_LEVEL_MSB) { + for (int i = buffer.limit() - 2; i >= buffer.position(); i -= 2) { + if (Math.abs(buffer.getShort(i)) > silenceThresholdLevel) { // Return the start of the next frame. return bytesPerFrame * (i / bytesPerFrame) + bytesPerFrame; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 553dfb118..30c664f0f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio; import android.media.audiofx.Virtualizer; import android.os.Handler; -import android.os.Looper; import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -29,6 +28,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; @@ -95,9 +95,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; private final AudioSink audioSink; - private final FormatHolder formatHolder; private final DecoderInputBuffer flagsOnlyBuffer; + private boolean drmResourcesAcquired; private DecoderCounters decoderCounters; private Format inputFormat; private int encoderDelay; @@ -213,40 +213,42 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.audioSink = audioSink; audioSink.setListener(new AudioSinkListener()); - formatHolder = new FormatHolder(); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); decoderReinitializationState = REINITIALIZATION_STATE_NONE; audioTrackNeedsConfigure = true; } @Override + @Nullable public MediaClock getMediaClock() { return this; } @Override + @Capabilities public final int supportsFormat(Format format) { if (!MimeTypes.isAudio(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } - int formatSupport = supportsFormatInternal(drmSessionManager, format); + @FormatSupport int formatSupport = supportsFormatInternal(drmSessionManager, format); if (formatSupport <= FORMAT_UNSUPPORTED_DRM) { - return formatSupport; + return RendererCapabilities.create(formatSupport); } + @TunnelingSupport int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport); } /** - * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for {@link - * #supportsFormat(Format)}. + * Returns the {@link FormatSupport} for the given {@link Format}. * * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format, which has an audio {@link Format#sampleMimeType}. - * @return The extent to which the renderer supports the format itself. + * @return The {@link FormatSupport} for this {@link Format}. */ + @FormatSupport protected abstract int supportsFormatInternal( - DrmSessionManager drmSessionManager, Format format); + @Nullable DrmSessionManager drmSessionManager, Format format); /** * Returns whether the sink supports the audio format. @@ -263,7 +265,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return; } @@ -271,10 +273,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements // Try and read a format if we don't have one already. if (inputFormat == null) { // We don't have a format yet, so try and read one. + FormatHolder formatHolder = getFormatHolder(); flagsOnlyBuffer.clear(); int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); } else if (result == C.RESULT_BUFFER_READ) { // End of stream read having not read a format. Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); @@ -299,7 +302,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements TraceUtil.endSection(); } catch (AudioDecoderException | AudioSink.ConfigurationException | AudioSink.InitializationException | AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } decoderCounters.ensureUpdated(); } @@ -341,21 +344,26 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @return The decoder. * @throws AudioDecoderException If an error occurred creating a suitable decoder. */ - protected abstract SimpleDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) - throws AudioDecoderException; + protected abstract SimpleDecoder< + DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws AudioDecoderException; /** * Returns the format of audio buffers output by the decoder. Will not be called until the first * output buffer has been dequeued, so the decoder may use input data to determine the format. - *

    - * The default implementation returns a 16-bit PCM format with the same channel count and sample - * rate as the input. */ - protected Format getOutputFormat() { - return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT, - null, null, 0, null); + protected abstract Format getOutputFormat(); + + /** + * Returns whether the existing decoder can be kept for a new format. + * + * @param oldFormat The previous format. + * @param newFormat The new format. + * @return True if the existing decoder can be kept. + */ + protected boolean canKeepCodec(Format oldFormat, Format newFormat) { + return false; } private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, @@ -427,6 +435,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } int result; + FormatHolder formatHolder = getFormatHolder(); if (waitingForKeys) { // We've already read an encrypted sample into buffer, and are waiting for keys. result = C.RESULT_BUFFER_READ; @@ -438,7 +447,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } if (inputBuffer.isEndOfStream()) { @@ -462,12 +471,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + if (decoderDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { return false; } @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); + throw createRendererException(decoderDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -477,7 +488,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements try { audioSink.playToEndOfStream(); } catch (AudioSink.WriteException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + // TODO(internal: b/145658993) Use outputFormat for the call from drainOutputBuffer. + throw createRendererException(e, inputFormat); } } @@ -517,8 +529,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioSink.setPlaybackParameters(playbackParameters); + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + audioSink.setPlaybackParameters(playbackParameters); } @Override @@ -528,6 +540,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { + if (drmSessionManager != null && !drmResourcesAcquired) { + drmResourcesAcquired = true; + drmSessionManager.prepare(); + } decoderCounters = new DecoderCounters(); eventDispatcher.enabled(decoderCounters); int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; @@ -576,6 +592,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } + @Override + protected void onReset() { + if (drmSessionManager != null && drmResourcesAcquired) { + drmResourcesAcquired = false; + drmSessionManager.release(); + } + } + @Override public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException { switch (messageType) { @@ -628,7 +652,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements codecInitializedTimestamp - codecInitializingTimestamp); decoderCounters.decoderInitCount++; } catch (AudioDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } @@ -646,62 +670,43 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession previous = sourceDrmSession; + DrmSession.replaceSession(sourceDrmSession, session); sourceDrmSession = session; - releaseDrmSessionIfUnused(previous); } private void setDecoderDrmSession(@Nullable DrmSession session) { - DrmSession previous = decoderDrmSession; + DrmSession.replaceSession(decoderDrmSession, session); decoderDrmSession = session; - releaseDrmSessionIfUnused(previous); } - private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { - if (session != null && session != decoderDrmSession && session != sourceDrmSession) { - drmSessionManager.releaseSession(session); + @SuppressWarnings("unchecked") + private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + Format newFormat = Assertions.checkNotNull(formatHolder.format); + if (formatHolder.includesDrmSession) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + sourceDrmSession = + getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); } - } - - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = inputFormat; inputFormat = newFormat; - boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null - : oldFormat.drmInitData); - if (drmInitDataChanged) { - if (inputFormat.drmInitData != null) { - if (drmSessionManager == null) { - throw ExoPlaybackException.createForRenderer( - new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); - } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (session == decoderDrmSession || session == sourceDrmSession) { - // We already had this session. The manager must be reference counting, so release it once - // to get the count attributed to this renderer back down to 1. - drmSessionManager.releaseSession(session); - } - setSourceDrmSession(session); + if (!canKeepCodec(oldFormat, inputFormat)) { + if (decoderReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; } else { - setSourceDrmSession(null); + // There aren't any final output buffers, so release the decoder immediately. + releaseDecoder(); + maybeInitDecoder(); + audioTrackNeedsConfigure = true; } } - if (decoderReceivedBuffers) { - // Signal end of stream and wait for any final output buffers before re-initialization. - decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; - } else { - // There aren't any final output buffers, so release the decoder immediately. - releaseDecoder(); - maybeInitDecoder(); - audioTrackNeedsConfigure = true; - } + encoderDelay = inputFormat.encoderDelay; + encoderPadding = inputFormat.encoderPadding; - encoderDelay = newFormat.encoderDelay; - encoderPadding = newFormat.encoderPadding; - - eventDispatcher.inputFormatChanged(newFormat); + eventDispatcher.inputFormatChanged(inputFormat); } private void onQueueInputBuffer(DecoderInputBuffer buffer) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java index bd32e5ee6..b9a59cd62 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/SonicAudioProcessor.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.audio; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.C.Encoding; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -62,12 +61,14 @@ public final class SonicAudioProcessor implements AudioProcessor { */ private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024; - private int channelCount; - private int sampleRateHz; + private int pendingOutputSampleRate; private float speed; private float pitch; - private int outputSampleRateHz; - private int pendingOutputSampleRateHz; + + private AudioFormat pendingInputAudioFormat; + private AudioFormat pendingOutputAudioFormat; + private AudioFormat inputAudioFormat; + private AudioFormat outputAudioFormat; private boolean pendingSonicRecreation; @Nullable private Sonic sonic; @@ -84,18 +85,20 @@ public final class SonicAudioProcessor implements AudioProcessor { public SonicAudioProcessor() { speed = 1f; pitch = 1f; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; - outputSampleRateHz = Format.NO_VALUE; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; shortBuffer = buffer.asShortBuffer(); outputBuffer = EMPTY_BUFFER; - pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE; + pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE; } /** - * Sets the playback speed. Calling this method will discard any data buffered within the - * processor, and may update the value returned by {@link #isActive()}. + * Sets the playback speed. This method may only be called after draining data through the + * processor. The value returned by {@link #isActive()} may change, and the processor must be + * {@link #flush() flushed} before queueing more data. * * @param speed The requested new playback speed. * @return The actual new playback speed. @@ -106,13 +109,13 @@ public final class SonicAudioProcessor implements AudioProcessor { this.speed = speed; pendingSonicRecreation = true; } - flush(); return speed; } /** - * Sets the playback pitch. Calling this method will discard any data buffered within the - * processor, and may update the value returned by {@link #isActive()}. + * Sets the playback pitch. This method may only be called after draining data through the + * processor. The value returned by {@link #isActive()} may change, and the processor must be + * {@link #flush() flushed} before queueing more data. * * @param pitch The requested new pitch. * @return The actual new pitch. @@ -123,20 +126,19 @@ public final class SonicAudioProcessor implements AudioProcessor { this.pitch = pitch; pendingSonicRecreation = true; } - flush(); return pitch; } /** - * Sets the sample rate for output audio, in hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output - * audio at the same sample rate as the input. After calling this method, call - * {@link #configure(int, int, int)} to start using the new sample rate. + * Sets the sample rate for output audio, in Hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output + * audio at the same sample rate as the input. After calling this method, call {@link + * #configure(AudioFormat)} to configure the processor with the new sample rate. * - * @param sampleRateHz The sample rate for output audio, in hertz. - * @see #configure(int, int, int) + * @param sampleRateHz The sample rate for output audio, in Hertz. + * @see #configure(AudioFormat) */ public void setOutputSampleRateHz(int sampleRateHz) { - pendingOutputSampleRateHz = sampleRateHz; + pendingOutputSampleRate = sampleRateHz; } /** @@ -149,55 +151,39 @@ public final class SonicAudioProcessor implements AudioProcessor { */ public long scaleDurationForSpeedup(long duration) { if (outputBytes >= MIN_BYTES_FOR_SPEEDUP_CALCULATION) { - return outputSampleRateHz == sampleRateHz + return outputAudioFormat.sampleRate == inputAudioFormat.sampleRate ? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes) - : Util.scaleLargeTimestamp(duration, inputBytes * outputSampleRateHz, - outputBytes * sampleRateHz); + : Util.scaleLargeTimestamp( + duration, + inputBytes * outputAudioFormat.sampleRate, + outputBytes * inputAudioFormat.sampleRate); } else { return (long) ((double) speed * duration); } } @Override - public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding) - throws UnhandledFormatException { - if (encoding != C.ENCODING_PCM_16BIT) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - int outputSampleRateHz = pendingOutputSampleRateHz == SAMPLE_RATE_NO_CHANGE - ? sampleRateHz : pendingOutputSampleRateHz; - if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount - && this.outputSampleRateHz == outputSampleRateHz) { - return false; - } - this.sampleRateHz = sampleRateHz; - this.channelCount = channelCount; - this.outputSampleRateHz = outputSampleRateHz; + int outputSampleRateHz = + pendingOutputSampleRate == SAMPLE_RATE_NO_CHANGE + ? inputAudioFormat.sampleRate + : pendingOutputSampleRate; + pendingInputAudioFormat = inputAudioFormat; + pendingOutputAudioFormat = + new AudioFormat(outputSampleRateHz, inputAudioFormat.channelCount, C.ENCODING_PCM_16BIT); pendingSonicRecreation = true; - return true; + return pendingOutputAudioFormat; } @Override public boolean isActive() { - return sampleRateHz != Format.NO_VALUE + return pendingOutputAudioFormat.sampleRate != Format.NO_VALUE && (Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD - || outputSampleRateHz != sampleRateHz); - } - - @Override - public int getOutputChannelCount() { - return channelCount; - } - - @Override - public int getOutputEncoding() { - return C.ENCODING_PCM_16BIT; - } - - @Override - public int getOutputSampleRateHz() { - return outputSampleRateHz; + || pendingOutputAudioFormat.sampleRate != pendingInputAudioFormat.sampleRate); } @Override @@ -249,8 +235,16 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void flush() { if (isActive()) { + inputAudioFormat = pendingInputAudioFormat; + outputAudioFormat = pendingOutputAudioFormat; if (pendingSonicRecreation) { - sonic = new Sonic(sampleRateHz, channelCount, speed, pitch, outputSampleRateHz); + sonic = + new Sonic( + inputAudioFormat.sampleRate, + inputAudioFormat.channelCount, + speed, + pitch, + outputAudioFormat.sampleRate); } else if (sonic != null) { sonic.flush(); } @@ -265,13 +259,14 @@ public final class SonicAudioProcessor implements AudioProcessor { public void reset() { speed = 1f; pitch = 1f; - channelCount = Format.NO_VALUE; - sampleRateHz = Format.NO_VALUE; - outputSampleRateHz = Format.NO_VALUE; + pendingInputAudioFormat = AudioFormat.NOT_SET; + pendingOutputAudioFormat = AudioFormat.NOT_SET; + inputAudioFormat = AudioFormat.NOT_SET; + outputAudioFormat = AudioFormat.NOT_SET; buffer = EMPTY_BUFFER; shortBuffer = buffer.asShortBuffer(); outputBuffer = EMPTY_BUFFER; - pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE; + pendingOutputSampleRate = SAMPLE_RATE_NO_CHANGE; pendingSonicRecreation = false; sonic = null; inputBytes = 0; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java index 6e4c97701..a9afa4719 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/TeeAudioProcessor.java @@ -64,8 +64,9 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { } @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) { - return setInputFormat(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) { + // This processor is always active (if passed to the sink) and outputs its input. + return inputAudioFormat; } @Override @@ -80,8 +81,23 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { @Override protected void onFlush() { + flushSinkIfActive(); + } + + @Override + protected void onQueueEndOfStream() { + flushSinkIfActive(); + } + + @Override + protected void onReset() { + flushSinkIfActive(); + } + + private void flushSinkIfActive() { if (isActive()) { - audioBufferSink.flush(sampleRateHz, channelCount, encoding); + audioBufferSink.flush( + inputAudioFormat.sampleRate, inputAudioFormat.channelCount, inputAudioFormat.encoding); } } @@ -165,7 +181,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { // Write the rest of the header as little endian data. scratchByteBuffer.clear(); scratchByteBuffer.putInt(16); - scratchByteBuffer.putShort((short) WavUtil.getTypeForEncoding(encoding)); + scratchByteBuffer.putShort((short) WavUtil.getTypeForPcmEncoding(encoding)); scratchByteBuffer.putShort((short) channelCount); scratchByteBuffer.putInt(sampleRateHz); int bytesPerSample = Util.getPcmFrameSize(encoding, channelCount); @@ -190,7 +206,7 @@ public final class TeeAudioProcessor extends BaseAudioProcessor { } private void reset() throws IOException { - RandomAccessFile randomAccessFile = this.randomAccessFile; + @Nullable RandomAccessFile randomAccessFile = this.randomAccessFile; if (randomAccessFile == null) { return; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index c9e9f921c..f630c267e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -24,11 +24,9 @@ import java.nio.ByteBuffer; @C.PcmEncoding private static final int OUTPUT_ENCODING = C.ENCODING_PCM_16BIT; - private boolean isActive; private int trimStartFrames; private int trimEndFrames; - private int bytesPerFrame; - private boolean receivedInputSinceConfigure; + private boolean reconfigurationPending; private int pendingTrimStartBytes; private byte[] endBuffer; @@ -42,7 +40,7 @@ import java.nio.ByteBuffer; /** * Sets the number of audio frames to trim from the start and end of audio passed to this - * processor. After calling this method, call {@link #configure(int, int, int)} to apply the new + * processor. After calling this method, call {@link #configure(AudioFormat)} to apply the new * trimming frame counts. * * @param trimStartFrames The number of audio frames to trim from the start of audio. @@ -68,28 +66,13 @@ import java.nio.ByteBuffer; } @Override - public boolean configure(int sampleRateHz, int channelCount, @C.PcmEncoding int encoding) - throws UnhandledFormatException { - if (encoding != OUTPUT_ENCODING) { - throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); + public AudioFormat onConfigure(AudioFormat inputAudioFormat) + throws UnhandledAudioFormatException { + if (inputAudioFormat.encoding != OUTPUT_ENCODING) { + throw new UnhandledAudioFormatException(inputAudioFormat); } - if (endBufferSize > 0) { - trimmedFrameCount += endBufferSize / bytesPerFrame; - } - bytesPerFrame = Util.getPcmFrameSize(OUTPUT_ENCODING, channelCount); - endBuffer = new byte[trimEndFrames * bytesPerFrame]; - endBufferSize = 0; - pendingTrimStartBytes = trimStartFrames * bytesPerFrame; - boolean wasActive = isActive; - isActive = trimStartFrames != 0 || trimEndFrames != 0; - receivedInputSinceConfigure = false; - setInputFormat(sampleRateHz, channelCount, encoding); - return wasActive != isActive; - } - - @Override - public boolean isActive() { - return isActive; + reconfigurationPending = true; + return trimStartFrames != 0 || trimEndFrames != 0 ? inputAudioFormat : AudioFormat.NOT_SET; } @Override @@ -101,11 +84,10 @@ import java.nio.ByteBuffer; if (remaining == 0) { return; } - receivedInputSinceConfigure = true; // Trim any pending start bytes from the input buffer. int trimBytes = Math.min(remaining, pendingTrimStartBytes); - trimmedFrameCount += trimBytes / bytesPerFrame; + trimmedFrameCount += trimBytes / inputAudioFormat.bytesPerFrame; pendingTrimStartBytes -= trimBytes; inputBuffer.position(position + trimBytes); if (pendingTrimStartBytes > 0) { @@ -142,37 +124,51 @@ import java.nio.ByteBuffer; buffer.flip(); } - @SuppressWarnings("ReferenceEquality") @Override public ByteBuffer getOutput() { if (super.isEnded() && endBufferSize > 0) { // Because audio processors may be drained in the middle of the stream we assume that the - // contents of the end buffer need to be output. For gapless transitions, configure will be - // always be called, which clears the end buffer as needed. When audio is actually ending we - // play the padding data which is incorrect. This behavior can be fixed once we have the - // timestamps associated with input buffers. + // contents of the end buffer need to be output. For gapless transitions, configure will + // always be called, so the end buffer is cleared in onQueueEndOfStream. replaceOutputBuffer(endBufferSize).put(endBuffer, 0, endBufferSize).flip(); endBufferSize = 0; } return super.getOutput(); } - @SuppressWarnings("ReferenceEquality") @Override public boolean isEnded() { return super.isEnded() && endBufferSize == 0; } @Override - protected void onFlush() { - if (receivedInputSinceConfigure) { - // Audio processors are flushed after initial configuration, so we leave the pending trim - // start byte count unmodified if the processor was just configured. Otherwise we (possibly - // incorrectly) assume that this is a seek to a non-zero position. We should instead check the - // timestamp of the first input buffer queued after flushing to decide whether to trim (see - // also [Internal: b/77292509]). - pendingTrimStartBytes = 0; + protected void onQueueEndOfStream() { + if (reconfigurationPending) { + // Trim audio in the end buffer. + if (endBufferSize > 0) { + trimmedFrameCount += endBufferSize / inputAudioFormat.bytesPerFrame; + } + endBufferSize = 0; } + } + + @Override + protected void onFlush() { + if (reconfigurationPending) { + // Flushing activates the new configuration, so prepare to trim bytes from the start/end. + reconfigurationPending = false; + endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame]; + pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame; + } + + // TODO(internal b/77292509): Flushing occurs to activate a configuration (handled above) but + // also when seeking within a stream. This implementation currently doesn't handle seek to start + // (where we need to trim at the start again), nor seeks to non-zero positions before start + // trimming has occurred (where we should set pendingTrimStartBytes to zero). These cases can be + // fixed by trimming in queueInput based on timestamp, once that information is available. + + // Any data in the end buffer should no longer be output if we are playing from a different + // position, so discard it and refill the buffer using new input. endBufferSize = 0; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index 473a91fed..208989124 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -23,39 +23,45 @@ import com.google.android.exoplayer2.util.Util; public final class WavUtil { /** Four character code for "RIFF". */ - public static final int RIFF_FOURCC = Util.getIntegerCodeForString("RIFF"); + public static final int RIFF_FOURCC = 0x52494646; /** Four character code for "WAVE". */ - public static final int WAVE_FOURCC = Util.getIntegerCodeForString("WAVE"); + public static final int WAVE_FOURCC = 0x57415645; /** Four character code for "fmt ". */ - public static final int FMT_FOURCC = Util.getIntegerCodeForString("fmt "); + public static final int FMT_FOURCC = 0x666d7420; /** Four character code for "data". */ - public static final int DATA_FOURCC = Util.getIntegerCodeForString("data"); + public static final int DATA_FOURCC = 0x64617461; /** WAVE type value for integer PCM audio data. */ - private static final int TYPE_PCM = 0x0001; + public static final int TYPE_PCM = 0x0001; /** WAVE type value for float PCM audio data. */ - private static final int TYPE_FLOAT = 0x0003; + public static final int TYPE_FLOAT = 0x0003; /** WAVE type value for 8-bit ITU-T G.711 A-law audio data. */ - private static final int TYPE_A_LAW = 0x0006; + public static final int TYPE_ALAW = 0x0006; /** WAVE type value for 8-bit ITU-T G.711 mu-law audio data. */ - private static final int TYPE_MU_LAW = 0x0007; + public static final int TYPE_MLAW = 0x0007; + /** WAVE type value for IMA ADPCM audio data. */ + public static final int TYPE_IMA_ADPCM = 0x0011; /** WAVE type value for extended WAVE format. */ - private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + public static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; - /** Returns the WAVE type value for the given {@code encoding}. */ - public static int getTypeForEncoding(@C.PcmEncoding int encoding) { - switch (encoding) { + /** + * Returns the WAVE format type value for the given {@link C.PcmEncoding}. + * + * @param pcmEncoding The {@link C.PcmEncoding} value. + * @return The corresponding WAVE format type. + * @throws IllegalArgumentException If {@code pcmEncoding} is not a {@link C.PcmEncoding}, or if + * it's {@link C#ENCODING_INVALID} or {@link Format#NO_VALUE}. + */ + public static int getTypeForPcmEncoding(@C.PcmEncoding int pcmEncoding) { + switch (pcmEncoding) { case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_32BIT: return TYPE_PCM; - case C.ENCODING_PCM_A_LAW: - return TYPE_A_LAW; - case C.ENCODING_PCM_MU_LAW: - return TYPE_MU_LAW; case C.ENCODING_PCM_FLOAT: return TYPE_FLOAT; + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: // Not TYPE_PCM, because TYPE_PCM is little endian. case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -63,18 +69,17 @@ public final class WavUtil { } } - /** Returns the PCM encoding for the given WAVE {@code type} value. */ - public static @C.PcmEncoding int getEncodingForType(int type, int bitsPerSample) { + /** + * Returns the {@link C.PcmEncoding} for the given WAVE format type value, or {@link + * C#ENCODING_INVALID} if the type is not a known PCM type. + */ + public static @C.PcmEncoding int getPcmEncodingForType(int type, int bitsPerSample) { switch (type) { case TYPE_PCM: case TYPE_WAVE_FORMAT_EXTENSIBLE: return Util.getPcmEncoding(bitsPerSample); case TYPE_FLOAT: return bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; - case TYPE_A_LAW: - return C.ENCODING_PCM_A_LAW; - case TYPE_MU_LAW: - return C.ENCODING_PCM_MU_LAW; default: return C.ENCODING_INVALID; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/package-info.java new file mode 100644 index 000000000..5ae2413d9 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/audio/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.audio; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/database/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/database/package-info.java new file mode 100644 index 000000000..4921e1aee --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/database/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.database; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java index 773959fbf..8fd25f2cf 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/Buffer.java @@ -53,6 +53,11 @@ public abstract class Buffer { return getFlag(C.BUFFER_FLAG_KEY_FRAME); } + /** Returns whether the {@link C#BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA} flag is set. */ + public final boolean hasSupplementalData() { + return getFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA); + } + /** * Replaces this buffer's flags with {@code flags}. * diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java index 379ca971b..b865d5bb6 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java @@ -25,27 +25,41 @@ import com.google.android.exoplayer2.util.Util; public final class CryptoInfo { /** + * The 16 byte initialization vector. If the initialization vector of the content is shorter than + * 16 bytes, 0 byte padding is appended to extend the vector to the required 16 byte length. + * * @see android.media.MediaCodec.CryptoInfo#iv */ public byte[] iv; /** + * The 16 byte key id. + * * @see android.media.MediaCodec.CryptoInfo#key */ public byte[] key; /** + * The type of encryption that has been applied. Must be one of the {@link C.CryptoMode} values. + * * @see android.media.MediaCodec.CryptoInfo#mode */ - @C.CryptoMode - public int mode; + @C.CryptoMode public int mode; /** + * The number of leading unencrypted bytes in each sub-sample. If null, all bytes are treated as + * encrypted and {@link #numBytesOfEncryptedData} must be specified. + * * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData */ public int[] numBytesOfClearData; /** + * The number of trailing encrypted bytes in each sub-sample. If null, all bytes are treated as + * clear and {@link #numBytesOfClearData} must be specified. + * * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData */ public int[] numBytesOfEncryptedData; /** + * The number of subSamples that make up the buffer's contents. + * * @see android.media.MediaCodec.CryptoInfo#numSubSamples */ public int numSubSamples; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java index 7eb1fa1aa..4552d190c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/Decoder.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.decoder; +import androidx.annotation.Nullable; + /** * A media decoder. * @@ -37,6 +39,7 @@ public interface Decoder { * @return The input buffer, which will have been cleared, or null if a buffer isn't available. * @throws E If a decoder error has occurred. */ + @Nullable I dequeueInputBuffer() throws E; /** @@ -53,6 +56,7 @@ public interface Decoder { * @return The output buffer, or null if an output buffer isn't available. * @throws E If a decoder error has occurred. */ + @Nullable O dequeueOutputBuffer() throws E; /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index 7fc6fb625..bd5df4c8b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -16,11 +16,13 @@ package com.google.android.exoplayer2.decoder; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * Holds input for a decoder. @@ -58,16 +60,28 @@ public class DecoderInputBuffer extends Buffer { */ public final CryptoInfo cryptoInfo; + /** The buffer's data, or {@code null} if no data has been set. */ + @Nullable public ByteBuffer data; + + // TODO: Remove this temporary signaling once end-of-stream propagation for clips using content + // protection is fixed. See [Internal: b/153326944] for details. /** - * The buffer's data, or {@code null} if no data has been set. + * Whether the last attempt to read a sample into this buffer failed due to not yet having the DRM + * keys associated with the next sample. */ - public ByteBuffer data; + public boolean waitingForKeys; /** * The time at which the sample should be presented. */ public long timeUs; + /** + * Supplemental data related to the buffer, if {@link #hasSupplementalData()} returns true. If + * present, the buffer is populated with supplemental data from position 0 to its limit. + */ + @Nullable public ByteBuffer supplementalData; + @BufferReplacementMode private final int bufferReplacementMode; /** @@ -89,6 +103,21 @@ public class DecoderInputBuffer extends Buffer { this.bufferReplacementMode = bufferReplacementMode; } + /** + * Clears {@link #supplementalData} and ensures that it's large enough to accommodate {@code + * length} bytes. + * + * @param length The length of the supplemental data that must be accommodated, in bytes. + */ + @EnsuresNonNull("supplementalData") + public void resetSupplementalData(int length) { + if (supplementalData == null || supplementalData.capacity() < length) { + supplementalData = ByteBuffer.allocate(length); + } else { + supplementalData.clear(); + } + } + /** * Ensures that {@link #data} is large enough to accommodate a write of a given length at its * current position. @@ -101,6 +130,7 @@ public class DecoderInputBuffer extends Buffer { * @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}. */ + @EnsuresNonNull("data") public void ensureSpaceForWrite(int length) { if (data == null) { data = createReplacementByteBuffer(length); @@ -115,10 +145,10 @@ public class DecoderInputBuffer extends Buffer { } // Instantiate a new buffer if possible. ByteBuffer newData = createReplacementByteBuffer(requiredCapacity); + newData.order(data.order()); // Copy data up to the current position from the old buffer to the new one. if (position > 0) { - data.position(0); - data.limit(position); + data.flip(); newData.put(data); } // Set the new buffer. @@ -141,12 +171,15 @@ public class DecoderInputBuffer extends Buffer { } /** - * Flips {@link #data} in preparation for being queued to a decoder. + * Flips {@link #data} and {@link #supplementalData} in preparation for being queued to a decoder. * * @see java.nio.Buffer#flip() */ public final void flip() { data.flip(); + if (supplementalData != null) { + supplementalData.flip(); + } } @Override @@ -155,6 +188,10 @@ public class DecoderInputBuffer extends Buffer { if (data != null) { data.clear(); } + if (supplementalData != null) { + supplementalData.clear(); + } + waitingForKeys = false; } private ByteBuffer createReplacementByteBuffer(int requiredCapacity) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index b5650860e..4eef1ea32 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.decoder; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; @@ -86,6 +87,7 @@ public abstract class SimpleDecoder< } @Override + @Nullable public final I dequeueInputBuffer() throws E { synchronized (lock) { maybeThrowException(); @@ -108,6 +110,7 @@ public abstract class SimpleDecoder< } @Override + @Nullable public final O dequeueOutputBuffer() throws E { synchronized (lock) { maybeThrowException(); @@ -123,6 +126,7 @@ public abstract class SimpleDecoder< * * @param outputBuffer The output buffer being released. */ + @CallSuper protected void releaseOutputBuffer(O outputBuffer) { synchronized (lock) { releaseOutputBufferInternal(outputBuffer); @@ -145,9 +149,11 @@ public abstract class SimpleDecoder< while (!queuedOutputBuffers.isEmpty()) { queuedOutputBuffers.removeFirst().release(); } + exception = null; } } + @CallSuper @Override public void release() { synchronized (lock) { @@ -220,6 +226,7 @@ public abstract class SimpleDecoder< if (inputBuffer.isDecodeOnly()) { outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); } + @Nullable E exception; try { exception = decode(inputBuffer, outputBuffer, resetDecoder); } catch (RuntimeException e) { @@ -233,8 +240,9 @@ public abstract class SimpleDecoder< exception = createUnexpectedDecodeException(e); } if (exception != null) { - // Memory barrier to ensure that the decoder exception is visible from the playback thread. - synchronized (lock) {} + synchronized (lock) { + this.exception = exception; + } return false; } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java index 49c7dafbd..84cffc114 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.decoder; +import androidx.annotation.Nullable; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -25,7 +26,7 @@ public class SimpleOutputBuffer extends OutputBuffer { private final SimpleDecoder owner; - public ByteBuffer data; + @Nullable public ByteBuffer data; public SimpleOutputBuffer(SimpleDecoder owner) { this.owner = owner; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/package-info.java new file mode 100644 index 000000000..0c4dbde9d --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/decoder/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.decoder; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java deleted file mode 100644 index dbe5c9317..000000000 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DecryptionResource.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.drm; - -/** - * A reference-counted resource used in the decryption of media samples. - * - * @param The reference type with which to make {@link Owner#onLastReferenceReleased} calls. - * Subclasses are expected to pass themselves. - */ -public abstract class DecryptionResource> { - - /** - * Implemented by the class in charge of managing a {@link DecryptionResource resource's} - * lifecycle. - */ - public interface Owner> { - - /** - * Called when the last reference to a {@link DecryptionResource} is {@link #releaseReference() - * released}. - */ - void onLastReferenceReleased(T resource); - } - - // TODO: Consider adding a handler on which the owner should be called. - private final DecryptionResource.Owner owner; - private int referenceCount; - - /** - * Creates a new instance with reference count zero. - * - * @param owner The owner of this instance. - */ - public DecryptionResource(Owner owner) { - this.owner = owner; - referenceCount = 0; - } - - /** Increases by one the reference count for this resource. */ - public void acquireReference() { - referenceCount++; - } - - /** - * Decreases by one the reference count for this resource, and notifies the owner if said count - * reached zero as a result of this operation. - * - *

    Must only be called as releasing counter-part of {@link #acquireReference()}. - */ - @SuppressWarnings("unchecked") - public void releaseReference() { - if (--referenceCount == 0) { - owner.onLastReferenceReleased((T) this); - } else if (referenceCount < 0) { - throw new IllegalStateException("Illegal release of resource."); - } - } -} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 775d56f40..432cc6613 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -22,16 +22,19 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import androidx.annotation.Nullable; +import android.os.SystemClock; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.EventDispatcher; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -42,20 +45,24 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; -/** - * A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. - */ +/** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) /* package */ class DefaultDrmSession implements DrmSession { - /** - * Manages provisioning requests. - */ + /** Thrown when an unexpected exception or error is thrown during provisioning or key requests. */ + public static final class UnexpectedDrmSessionException extends IOException { + + public UnexpectedDrmSessionException(Throwable cause) { + super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); + } + } + + /** Manages provisioning requests. */ public interface ProvisioningManager { /** - * Called when a session requires provisioning. The manager may call - * {@link #provision()} to have this session perform the provisioning operation. The manager + * Called when a session requires provisioning. The manager may call {@link + * #provision()} to have this session perform the provisioning operation. The manager * will call {@link DefaultDrmSession#onProvisionCompleted()} when provisioning has * completed, or {@link DefaultDrmSession#onProvisionError} if provisioning fails. * @@ -70,44 +77,55 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ void onProvisionError(Exception error); - /** - * Called by a session when it successfully completes a provisioning operation. - */ + /** Called by a session when it successfully completes a provisioning operation. */ void onProvisionCompleted(); + } + /** Callback to be notified when the session is released. */ + public interface ReleaseCallback { + + /** + * Called immediately after releasing session resources. + * + * @param session The session. + */ + void onSessionReleased(DefaultDrmSession session); } private static final String TAG = "DefaultDrmSession"; private static final int MSG_PROVISION = 0; private static final int MSG_KEYS = 1; - private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; + private static final int MAX_LICENSE_DURATION_TO_RENEW_SECONDS = 60; /** The DRM scheme datas, or null if this session uses offline keys. */ - public final @Nullable List schemeDatas; + @Nullable public final List schemeDatas; private final ExoMediaDrm mediaDrm; private final ProvisioningManager provisioningManager; + private final ReleaseCallback releaseCallback; private final @DefaultDrmSessionManager.Mode int mode; - private final @Nullable HashMap optionalKeyRequestParameters; + private final boolean playClearSamplesWithoutKeys; + private final boolean isPlaceholderSession; + private final HashMap keyRequestParameters; private final EventDispatcher eventDispatcher; - private final int initialDrmRequestRetryCount; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; - /* package */ final PostResponseHandler postResponseHandler; + /* package */ final ResponseHandler responseHandler; private @DrmSession.State int state; - private int openCount; - private HandlerThread requestHandlerThread; - private PostRequestHandler postRequestHandler; - private @Nullable T mediaCrypto; - private @Nullable DrmSessionException lastException; - private byte @MonotonicNonNull [] sessionId; - private byte @MonotonicNonNull [] offlineLicenseKeySetId; + private int referenceCount; + @Nullable private HandlerThread requestHandlerThread; + @Nullable private RequestHandler requestHandler; + @Nullable private T mediaCrypto; + @Nullable private DrmSessionException lastException; + @Nullable private byte[] sessionId; + @MonotonicNonNull private byte[] offlineLicenseKeySetId; - private @Nullable KeyRequest currentKeyRequest; - private @Nullable ProvisionRequest currentProvisionRequest; + @Nullable private KeyRequest currentKeyRequest; + @Nullable private ProvisionRequest currentProvisionRequest; /** * Instantiates a new DRM session. @@ -115,92 +133,60 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param uuid The UUID of the drm scheme. * @param mediaDrm The media DRM. * @param provisioningManager The manager for provisioning. + * @param releaseCallback The {@link ReleaseCallback}. * @param schemeDatas DRM scheme datas for this session, or null if an {@code - * offlineLicenseKeySetId} is provided. - * @param mode The DRM mode. + * offlineLicenseKeySetId} is provided or if {@code isPlaceholderSession} is true. + * @param mode The DRM mode. Ignored if {@code isPlaceholderSession} is true. + * @param isPlaceholderSession Whether this session is not expected to acquire any keys. * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using * offline keys. - * @param optionalKeyRequestParameters The optional key request parameters. + * @param keyRequestParameters Key request parameters. * @param callback The media DRM callback. * @param playbackLooper The playback looper. * @param eventDispatcher The dispatcher for DRM session manager events. - * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and - * key request before reporting error. + * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for key and provisioning + * requests. */ + // the constructor does not initialize fields: sessionId + @SuppressWarnings("nullness:initialization.fields.uninitialized") public DefaultDrmSession( UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, + ReleaseCallback releaseCallback, @Nullable List schemeDatas, @DefaultDrmSessionManager.Mode int mode, + boolean playClearSamplesWithoutKeys, + boolean isPlaceholderSession, @Nullable byte[] offlineLicenseKeySetId, - @Nullable HashMap optionalKeyRequestParameters, + HashMap keyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, EventDispatcher eventDispatcher, - int initialDrmRequestRetryCount) { + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { if (mode == DefaultDrmSessionManager.MODE_QUERY || mode == DefaultDrmSessionManager.MODE_RELEASE) { Assertions.checkNotNull(offlineLicenseKeySetId); } this.uuid = uuid; this.provisioningManager = provisioningManager; + this.releaseCallback = releaseCallback; this.mediaDrm = mediaDrm; this.mode = mode; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.isPlaceholderSession = isPlaceholderSession; if (offlineLicenseKeySetId != null) { this.offlineLicenseKeySetId = offlineLicenseKeySetId; this.schemeDatas = null; } else { this.schemeDatas = Collections.unmodifiableList(Assertions.checkNotNull(schemeDatas)); } - this.optionalKeyRequestParameters = optionalKeyRequestParameters; + this.keyRequestParameters = keyRequestParameters; this.callback = callback; - this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; this.eventDispatcher = eventDispatcher; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; state = STATE_OPENING; - - postResponseHandler = new PostResponseHandler(playbackLooper); - requestHandlerThread = new HandlerThread("DrmRequestHandler"); - requestHandlerThread.start(); - postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - } - - // Life cycle. - - public void acquire() { - if (++openCount == 1) { - if (state == STATE_ERROR) { - return; - } - if (openInternal(true)) { - doLicense(true); - } - } - } - - /** @return True if the session is closed and cleaned up, false otherwise. */ - // Assigning null to various non-null variables for clean-up. Class won't be used after release. - @SuppressWarnings("assignment.type.incompatible") - public boolean release() { - if (--openCount == 0) { - state = STATE_RELEASED; - postResponseHandler.removeCallbacksAndMessages(null); - postRequestHandler.removeCallbacksAndMessages(null); - postRequestHandler = null; - requestHandlerThread.quit(); - requestHandlerThread = null; - mediaCrypto = null; - lastException = null; - currentKeyRequest = null; - currentProvisionRequest = null; - if (sessionId != null) { - mediaDrm.closeSession(sessionId); - sessionId = null; - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); - } - return true; - } - return false; + responseHandler = new ResponseHandler(playbackLooper); } public boolean hasSessionId(byte[] sessionId) { @@ -221,7 +207,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public void provision() { currentProvisionRequest = mediaDrm.getProvisionRequest(); - postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true); + Util.castNonNull(requestHandler) + .post( + MSG_PROVISION, + Assertions.checkNotNull(currentProvisionRequest), + /* allowRetry= */ true); } public void onProvisionCompleted() { @@ -242,6 +232,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return state; } + @Override + public boolean playClearSamplesWithoutKeys() { + return playClearSamplesWithoutKeys; + } + @Override public final @Nullable DrmSessionException getError() { return state == STATE_ERROR ? lastException : null; @@ -253,15 +248,54 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public @Nullable Map queryKeyStatus() { + @Nullable + public Map queryKeyStatus() { return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); } @Override - public @Nullable byte[] getOfflineLicenseKeySetId() { + @Nullable + public byte[] getOfflineLicenseKeySetId() { return offlineLicenseKeySetId; } + @Override + public void acquire() { + Assertions.checkState(referenceCount >= 0); + if (++referenceCount == 1) { + Assertions.checkState(state == STATE_OPENING); + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + requestHandler = new RequestHandler(requestHandlerThread.getLooper()); + if (openInternal(true)) { + doLicense(true); + } + } + } + + @Override + public void release() { + if (--referenceCount == 0) { + // Assigning null to various non-null variables for clean-up. + state = STATE_RELEASED; + Util.castNonNull(responseHandler).removeCallbacksAndMessages(null); + Util.castNonNull(requestHandler).removeCallbacksAndMessages(null); + requestHandler = null; + Util.castNonNull(requestHandlerThread).quit(); + requestHandlerThread = null; + mediaCrypto = null; + lastException = null; + currentKeyRequest = null; + currentProvisionRequest = null; + if (sessionId != null) { + mediaDrm.closeSession(sessionId); + sessionId = null; + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionReleased); + } + releaseCallback.onSessionReleased(this); + } + } + // Internal methods. /** @@ -280,9 +314,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; try { sessionId = mediaDrm.openSession(); - eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired); mediaCrypto = mediaDrm.createMediaCrypto(sessionId); + eventDispatcher.dispatch(DefaultDrmSessionEventListener::onDrmSessionAcquired); state = STATE_OPENED; + Assertions.checkNotNull(sessionId); return true; } catch (NotProvisionedException e) { if (allowProvisioning) { @@ -321,6 +356,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @RequiresNonNull("sessionId") private void doLicense(boolean allowRetry) { + if (isPlaceholderSession) { + return; + } + byte[] sessionId = Util.castNonNull(this.sessionId); switch (mode) { case DefaultDrmSessionManager.MODE_PLAYBACK: case DefaultDrmSessionManager.MODE_QUERY: @@ -329,9 +368,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } else if (state == STATE_OPENED_WITH_KEYS || restoreKeys()) { long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); if (mode == DefaultDrmSessionManager.MODE_PLAYBACK - && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { - Log.d(TAG, "Offline license has expired or will expire soon. " - + "Remaining seconds: " + licenseDurationRemainingSec); + && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW_SECONDS) { + Log.d( + TAG, + "Offline license has expired or will expire soon. " + + "Remaining seconds: " + + licenseDurationRemainingSec); postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); } else if (licenseDurationRemainingSec <= 0) { onError(new KeysExpiredException()); @@ -342,17 +384,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } break; case DefaultDrmSessionManager.MODE_DOWNLOAD: - if (offlineLicenseKeySetId == null) { + if (offlineLicenseKeySetId == null || restoreKeys()) { postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); - } else { - // Renew - if (restoreKeys()) { - postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); - } } break; case DefaultDrmSessionManager.MODE_RELEASE: Assertions.checkNotNull(offlineLicenseKeySetId); + Assertions.checkNotNull(this.sessionId); // It's not necessary to restore the key (and open a session to do that) before releasing it // but this serves as a good sanity/fast-failure check. if (restoreKeys()) { @@ -370,7 +408,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); return true; } catch (Exception e) { - Log.e(TAG, "Error trying to restore Widevine keys.", e); + Log.e(TAG, "Error trying to restore keys.", e); onError(e); } return false; @@ -387,9 +425,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private void postKeyRequest(byte[] scope, int type, boolean allowRetry) { try { - currentKeyRequest = - mediaDrm.getKeyRequest(scope, schemeDatas, type, optionalKeyRequestParameters); - postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry); + currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters); + Util.castNonNull(requestHandler) + .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry); } catch (Exception e) { onKeysError(e); } @@ -415,8 +453,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } else { byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD - || (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null)) - && keySetId != null && keySetId.length != 0) { + || (mode == DefaultDrmSessionManager.MODE_PLAYBACK + && offlineLicenseKeySetId != null)) + && keySetId != null + && keySetId.length != 0) { offlineLicenseKeySetId = keySetId; } state = STATE_OPENED_WITH_KEYS; @@ -459,9 +499,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // Internal classes. @SuppressLint("HandlerLeak") - private class PostResponseHandler extends Handler { + private class ResponseHandler extends Handler { - public PostResponseHandler(Looper looper) { + public ResponseHandler(Looper looper) { super(looper); } @@ -480,68 +520,88 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; break; default: break; - } } - } @SuppressLint("HandlerLeak") - private class PostRequestHandler extends Handler { + private class RequestHandler extends Handler { - public PostRequestHandler(Looper backgroundLooper) { + public RequestHandler(Looper backgroundLooper) { super(backgroundLooper); } void post(int what, Object request, boolean allowRetry) { - int allowRetryInt = allowRetry ? 1 : 0; - int errorCount = 0; - obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget(); + RequestTask requestTask = + new RequestTask(allowRetry, /* startTimeMs= */ SystemClock.elapsedRealtime(), request); + obtainMessage(what, requestTask).sendToTarget(); } @Override - @SuppressWarnings("unchecked") public void handleMessage(Message msg) { - Object request = msg.obj; + RequestTask requestTask = (RequestTask) msg.obj; Object response; try { switch (msg.what) { case MSG_PROVISION: - response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request); + response = + callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request); break; case MSG_KEYS: - response = callback.executeKeyRequest(uuid, (KeyRequest) request); + response = callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request); break; default: throw new RuntimeException(); } } catch (Exception e) { - if (maybeRetryRequest(msg)) { + if (maybeRetryRequest(msg, e)) { return; } response = e; } - postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget(); + responseHandler + .obtainMessage(msg.what, Pair.create(requestTask.request, response)) + .sendToTarget(); } - private boolean maybeRetryRequest(Message originalMsg) { - boolean allowRetry = originalMsg.arg1 == 1; - if (!allowRetry) { + private boolean maybeRetryRequest(Message originalMsg, Exception e) { + RequestTask requestTask = (RequestTask) originalMsg.obj; + if (!requestTask.allowRetry) { return false; } - int errorCount = originalMsg.arg2 + 1; - if (errorCount > initialDrmRequestRetryCount) { + requestTask.errorCount++; + if (requestTask.errorCount + > loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_DRM)) { return false; } - Message retryMsg = Message.obtain(originalMsg); - retryMsg.arg2 = errorCount; - sendMessageDelayed(retryMsg, getRetryDelayMillis(errorCount)); + IOException ioException = + e instanceof IOException ? (IOException) e : new UnexpectedDrmSessionException(e); + long retryDelayMs = + loadErrorHandlingPolicy.getRetryDelayMsFor( + C.DATA_TYPE_DRM, + /* loadDurationMs= */ SystemClock.elapsedRealtime() - requestTask.startTimeMs, + ioException, + requestTask.errorCount); + if (retryDelayMs == C.TIME_UNSET) { + // The error is fatal. + return false; + } + sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs); return true; } + } - private long getRetryDelayMillis(int errorCount) { - return Math.min((errorCount - 1) * 1000, 5000); + private static final class RequestTask { + + public final boolean allowRetry; + public final long startTimeMs; + public final Object request; + public int errorCount; + + public RequestTask(boolean allowRetry, long startTimeMs, Object request) { + this.allowRetry = allowRetry; + this.startTimeMs = startTimeMs; + this.request = request; } - } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java index fa5b60e66..297f26bb7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionEventListener.java @@ -24,7 +24,7 @@ public interface DefaultDrmSessionEventListener { default void onDrmSessionAcquired() {} /** Called each time keys are loaded. */ - void onDrmKeysLoaded(); + default void onDrmKeysLoaded() {} /** * Called when a drm error occurs. @@ -38,13 +38,13 @@ public interface DefaultDrmSessionEventListener { * * @param error The corresponding exception. */ - void onDrmSessionManagerError(Exception error); + default void onDrmSessionManagerError(Exception error) {} /** Called each time offline keys are restored. */ - void onDrmKeysRestored(); + default void onDrmKeysRestored() {} /** Called each time offline keys are removed. */ - void onDrmKeysRemoved(); + default void onDrmKeysRemoved() {} /** Called each time a drm session is released. */ default void onDrmSessionReleased() {} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index fb684f627..1c27d745d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -22,12 +22,12 @@ import android.os.Looper; import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.EventDispatcher; import com.google.android.exoplayer2.util.Log; @@ -36,16 +36,162 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; -/** - * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. - */ +/** A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) -public class DefaultDrmSessionManager implements DrmSessionManager, - ProvisioningManager { +public class DefaultDrmSessionManager implements DrmSessionManager { + + /** + * Builder for {@link DefaultDrmSessionManager} instances. + * + *

    See {@link #Builder} for the list of default values. + */ + public static final class Builder { + + private final HashMap keyRequestParameters; + private UUID uuid; + private ExoMediaDrm.Provider exoMediaDrmProvider; + private boolean multiSession; + private int[] useDrmSessionsForClearContentTrackTypes; + private boolean playClearSamplesWithoutKeys; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; + + /** + * Creates a builder with default values. The default values are: + * + *

      + *
    • {@link #setKeyRequestParameters keyRequestParameters}: An empty map. + *
    • {@link #setUuidAndExoMediaDrmProvider UUID}: {@link C#WIDEVINE_UUID}. + *
    • {@link #setUuidAndExoMediaDrmProvider ExoMediaDrm.Provider}: {@link + * FrameworkMediaDrm#DEFAULT_PROVIDER}. + *
    • {@link #setMultiSession multiSession}: {@code false}. + *
    • {@link #setUseDrmSessionsForClearContent useDrmSessionsForClearContent}: No tracks. + *
    • {@link #setPlayClearSamplesWithoutKeys playClearSamplesWithoutKeys}: {@code false}. + *
    • {@link #setLoadErrorHandlingPolicy LoadErrorHandlingPolicy}: {@link + * DefaultLoadErrorHandlingPolicy}. + *
    + */ + @SuppressWarnings("unchecked") + public Builder() { + keyRequestParameters = new HashMap<>(); + uuid = C.WIDEVINE_UUID; + exoMediaDrmProvider = (ExoMediaDrm.Provider) FrameworkMediaDrm.DEFAULT_PROVIDER; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + useDrmSessionsForClearContentTrackTypes = new int[0]; + } + + /** + * Sets the key request parameters to pass as the last argument to {@link + * ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. + * + *

    Custom data for PlayReady should be set under {@link #PLAYREADY_CUSTOM_DATA_KEY}. + * + * @param keyRequestParameters A map with parameters. + * @return This builder. + */ + public Builder setKeyRequestParameters(Map keyRequestParameters) { + this.keyRequestParameters.clear(); + this.keyRequestParameters.putAll(Assertions.checkNotNull(keyRequestParameters)); + return this; + } + + /** + * Sets the UUID of the DRM scheme and the {@link ExoMediaDrm.Provider} to use. + * + * @param uuid The UUID of the DRM scheme. + * @param exoMediaDrmProvider The {@link ExoMediaDrm.Provider}. + * @return This builder. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public Builder setUuidAndExoMediaDrmProvider( + UUID uuid, ExoMediaDrm.Provider exoMediaDrmProvider) { + this.uuid = Assertions.checkNotNull(uuid); + this.exoMediaDrmProvider = Assertions.checkNotNull(exoMediaDrmProvider); + return this; + } + + /** + * Sets whether this session manager is allowed to acquire multiple simultaneous sessions. + * + *

    Users should pass false when a single key request will obtain all keys required to decrypt + * the associated content. {@code multiSession} is required when content uses key rotation. + * + * @param multiSession Whether this session manager is allowed to acquire multiple simultaneous + * sessions. + * @return This builder. + */ + public Builder setMultiSession(boolean multiSession) { + this.multiSession = multiSession; + return this; + } + + /** + * Sets whether this session manager should attach {@link DrmSession DrmSessions} to the clear + * sections of the media content. + * + *

    Using {@link DrmSession DrmSessions} for clear content avoids the recreation of decoders + * when transitioning between clear and encrypted sections of content. + * + * @param useDrmSessionsForClearContentTrackTypes The track types ({@link C#TRACK_TYPE_AUDIO} + * and/or {@link C#TRACK_TYPE_VIDEO}) for which to use a {@link DrmSession} regardless of + * whether the content is clear or encrypted. + * @return This builder. + * @throws IllegalArgumentException If {@code useDrmSessionsForClearContentTrackTypes} contains + * track types other than {@link C#TRACK_TYPE_AUDIO} and {@link C#TRACK_TYPE_VIDEO}. + */ + public Builder setUseDrmSessionsForClearContent( + int... useDrmSessionsForClearContentTrackTypes) { + for (int trackType : useDrmSessionsForClearContentTrackTypes) { + Assertions.checkArgument( + trackType == C.TRACK_TYPE_VIDEO || trackType == C.TRACK_TYPE_AUDIO); + } + this.useDrmSessionsForClearContentTrackTypes = + useDrmSessionsForClearContentTrackTypes.clone(); + return this; + } + + /** + * Sets whether clear samples within protected content should be played when keys for the + * encrypted part of the content have yet to be loaded. + * + * @param playClearSamplesWithoutKeys Whether clear samples within protected content should be + * played when keys for the encrypted part of the content have yet to be loaded. + * @return This builder. + */ + public Builder setPlayClearSamplesWithoutKeys(boolean playClearSamplesWithoutKeys) { + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + return this; + } + + /** + * Sets the {@link LoadErrorHandlingPolicy} for key and provisioning requests. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This builder. + */ + public Builder setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + this.loadErrorHandlingPolicy = Assertions.checkNotNull(loadErrorHandlingPolicy); + return this; + } + + /** Builds a {@link DefaultDrmSessionManager} instance. */ + public DefaultDrmSessionManager build(MediaDrmCallback mediaDrmCallback) { + return new DefaultDrmSessionManager<>( + uuid, + exoMediaDrmProvider, + mediaDrmCallback, + keyRequestParameters, + multiSession, + useDrmSessionsForClearContentTrackTypes, + playClearSamplesWithoutKeys, + loadErrorHandlingPolicy); + } + } /** * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does @@ -59,7 +205,8 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. + * A key for specifying PlayReady custom data in the key request parameters passed to {@link + * Builder#setKeyRequestParameters(Map)}. */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; @@ -76,9 +223,7 @@ public class DefaultDrmSessionManager implements DrmSe * licenses. */ public static final int MODE_PLAYBACK = 0; - /** - * Restores an offline license to allow its status to be queried. - */ + /** Restores an offline license to allow its status to be queried. */ public static final int MODE_QUERY = 1; /** Downloads an offline license or renews an existing one. */ public static final int MODE_DOWNLOAD = 2; @@ -90,165 +235,136 @@ public class DefaultDrmSessionManager implements DrmSe private static final String TAG = "DefaultDrmSessionMgr"; private final UUID uuid; - private final ExoMediaDrm mediaDrm; + private final ExoMediaDrm.Provider exoMediaDrmProvider; private final MediaDrmCallback callback; - private final @Nullable HashMap optionalKeyRequestParameters; + private final HashMap keyRequestParameters; private final EventDispatcher eventDispatcher; private final boolean multiSession; - private final int initialDrmRequestRetryCount; + private final int[] useDrmSessionsForClearContentTrackTypes; + private final boolean playClearSamplesWithoutKeys; + private final ProvisioningManagerImpl provisioningManagerImpl; + private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final List> sessions; private final List> provisioningSessions; - private @Nullable Looper playbackLooper; + private int prepareCallsCount; + @Nullable private ExoMediaDrm exoMediaDrm; + @Nullable private DefaultDrmSession placeholderDrmSession; + @Nullable private DefaultDrmSession noMultiSessionDrmSession; + @Nullable private Looper playbackLooper; private int mode; - private @Nullable byte[] offlineLicenseKeySetId; + @Nullable private byte[] offlineLicenseKeySetId; /* package */ volatile @Nullable MediaDrmHandler mediaDrmHandler; /** - * Instantiates a new instance using the Widevine scheme. - * - * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. - */ - public static DefaultDrmSessionManager newWidevineInstance( - MediaDrmCallback callback, @Nullable HashMap optionalKeyRequestParameters) - throws UnsupportedDrmException { - return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters); - } - - /** - * 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 callback Performs key and provisioning requests. - * @param customData Optional custom data to include in requests generated by the instance. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. - */ - public static DefaultDrmSessionManager newPlayReadyInstance( - MediaDrmCallback callback, @Nullable String customData) throws UnsupportedDrmException { - HashMap optionalKeyRequestParameters; - if (!TextUtils.isEmpty(customData)) { - optionalKeyRequestParameters = new HashMap<>(); - optionalKeyRequestParameters.put(PLAYREADY_CUSTOM_DATA_KEY, customData); - } else { - optionalKeyRequestParameters = null; - } - return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters); - } - - /** - * Instantiates a new instance. - * * @param uuid The UUID of the drm scheme. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. - * @throws UnsupportedDrmException If the specified DRM scheme is not supported. - */ - public static DefaultDrmSessionManager newFrameworkInstance( - UUID uuid, - MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters) - throws UnsupportedDrmException { - return new DefaultDrmSessionManager<>( - uuid, - FrameworkMediaDrm.newInstance(uuid), - callback, - optionalKeyRequestParameters, - /* multiSession= */ false, - INITIAL_DRM_REQUEST_RETRY_COUNT); - } - - /** - * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. - * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @deprecated Use {@link Builder} instead. */ + @SuppressWarnings("deprecation") + @Deprecated public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm mediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters) { + @Nullable HashMap keyRequestParameters) { this( uuid, - mediaDrm, + exoMediaDrm, callback, - optionalKeyRequestParameters, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, /* multiSession= */ false, INITIAL_DRM_REQUEST_RETRY_COUNT); } /** * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. * @param multiSession A boolean that specify whether multiple key session support is enabled. * Default is false. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm mediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters, + @Nullable HashMap keyRequestParameters, boolean multiSession) { this( uuid, - mediaDrm, + exoMediaDrm, callback, - optionalKeyRequestParameters, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, multiSession, INITIAL_DRM_REQUEST_RETRY_COUNT); } /** * @param uuid The UUID of the drm scheme. - * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param exoMediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. - * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. + * @param keyRequestParameters An optional map of parameters to pass as the last argument to + * {@link ExoMediaDrm#getKeyRequest(byte[], List, int, HashMap)}. May be null. * @param multiSession A boolean that specify whether multiple key session support is enabled. * Default is false. * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and * key request before reporting error. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DefaultDrmSessionManager( UUID uuid, - ExoMediaDrm mediaDrm, + ExoMediaDrm exoMediaDrm, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters, + @Nullable HashMap keyRequestParameters, boolean multiSession, int initialDrmRequestRetryCount) { + this( + uuid, + new ExoMediaDrm.AppManagedProvider<>(exoMediaDrm), + callback, + keyRequestParameters == null ? new HashMap<>() : keyRequestParameters, + multiSession, + /* useDrmSessionsForClearContentTrackTypes= */ new int[0], + /* playClearSamplesWithoutKeys= */ false, + new DefaultLoadErrorHandlingPolicy(initialDrmRequestRetryCount)); + } + + // the constructor does not initialize fields: offlineLicenseKeySetId + @SuppressWarnings("nullness:initialization.fields.uninitialized") + private DefaultDrmSessionManager( + UUID uuid, + ExoMediaDrm.Provider exoMediaDrmProvider, + MediaDrmCallback callback, + HashMap keyRequestParameters, + boolean multiSession, + int[] useDrmSessionsForClearContentTrackTypes, + boolean playClearSamplesWithoutKeys, + LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkNotNull(uuid); - Assertions.checkNotNull(mediaDrm); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); this.uuid = uuid; - this.mediaDrm = mediaDrm; + this.exoMediaDrmProvider = exoMediaDrmProvider; this.callback = callback; - this.optionalKeyRequestParameters = optionalKeyRequestParameters; + this.keyRequestParameters = keyRequestParameters; this.eventDispatcher = new EventDispatcher<>(); this.multiSession = multiSession; - this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; + this.useDrmSessionsForClearContentTrackTypes = useDrmSessionsForClearContentTrackTypes; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + provisioningManagerImpl = new ProvisioningManagerImpl(); mode = MODE_PLAYBACK; sessions = new ArrayList<>(); provisioningSessions = new ArrayList<>(); - if (multiSession && C.WIDEVINE_UUID.equals(uuid) && Util.SDK_INT >= 19) { - // TODO: Enabling session sharing probably doesn't do anything useful here. It would only be - // useful if DefaultDrmSession instances were aware of one another's state, which is not - // implemented. Or if custom renderers are being used that allow playback to proceed before - // keys, which seems unlikely to be true in practice. - mediaDrm.setPropertyString("sessionSharing", "enable"); - } - mediaDrm.setOnEventListener(new MediaDrmEventListener()); } /** @@ -270,57 +386,10 @@ public class DefaultDrmSessionManager implements DrmSe eventDispatcher.removeListener(eventListener); } - /** - * Provides access to {@link ExoMediaDrm#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 ExoMediaDrm#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 ExoMediaDrm#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 ExoMediaDrm#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); - } - /** * Sets the mode, which determines the role of sessions acquired from the instance. This must be - * called before {@link #acquireSession(Looper, DrmInitData)} is called. + * called before {@link #acquireSession(Looper, DrmInitData)} or {@link + * #acquirePlaceholderSession} is called. * *

    By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when * required. @@ -352,6 +421,23 @@ public class DefaultDrmSessionManager implements DrmSe // DrmSessionManager implementation. + @Override + public final void prepare() { + if (prepareCallsCount++ == 0) { + Assertions.checkState(exoMediaDrm == null); + exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); + exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); + } + } + + @Override + public final void release() { + if (--prepareCallsCount == 0) { + Assertions.checkNotNull(exoMediaDrm).release(); + exoMediaDrm = null; + } + } + @Override public boolean canAcquireSession(DrmInitData drmInitData) { if (offlineLicenseKeySetId != null) { @@ -373,7 +459,8 @@ public class DefaultDrmSessionManager implements DrmSe if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { // If there is no scheme information, assume patternless AES-CTR. return true; - } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType) + } else if (C.CENC_TYPE_cbc1.equals(schemeType) + || C.CENC_TYPE_cbcs.equals(schemeType) || C.CENC_TYPE_cens.equals(schemeType)) { // API support for AES-CBC and pattern encryption was added in API 24. However, the // implementation was not stable until API 25. @@ -384,16 +471,37 @@ public class DefaultDrmSessionManager implements DrmSe } @Override - public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { - Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); - if (sessions.isEmpty()) { - this.playbackLooper = playbackLooper; - if (mediaDrmHandler == null) { - mediaDrmHandler = new MediaDrmHandler(playbackLooper); - } + @Nullable + public DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { + assertExpectedPlaybackLooper(playbackLooper); + ExoMediaDrm exoMediaDrm = Assertions.checkNotNull(this.exoMediaDrm); + boolean avoidPlaceholderDrmSessions = + FrameworkMediaCrypto.class.equals(exoMediaDrm.getExoMediaCryptoType()) + && FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC; + // Avoid attaching a session to sparse formats. + if (avoidPlaceholderDrmSessions + || Util.linearSearch(useDrmSessionsForClearContentTrackTypes, trackType) == C.INDEX_UNSET + || exoMediaDrm.getExoMediaCryptoType() == null) { + return null; } + maybeCreateMediaDrmHandler(playbackLooper); + if (placeholderDrmSession == null) { + DefaultDrmSession placeholderDrmSession = + createNewDefaultSession( + /* schemeDatas= */ Collections.emptyList(), /* isPlaceholderSession= */ true); + sessions.add(placeholderDrmSession); + this.placeholderDrmSession = placeholderDrmSession; + } + placeholderDrmSession.acquire(); + return placeholderDrmSession; + } - List schemeDatas = null; + @Override + public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { + assertExpectedPlaybackLooper(playbackLooper); + maybeCreateMediaDrmHandler(playbackLooper); + + @Nullable List schemeDatas = null; if (offlineLicenseKeySetId == null) { schemeDatas = getSchemeDatas(drmInitData, uuid, false); if (schemeDatas.isEmpty()) { @@ -403,9 +511,9 @@ public class DefaultDrmSessionManager implements DrmSe } } - DefaultDrmSession session; + @Nullable DefaultDrmSession session; if (!multiSession) { - session = sessions.isEmpty() ? null : sessions.get(0); + session = noMultiSessionDrmSession; } else { // Only use an existing session if it has matching init data. session = null; @@ -419,19 +527,10 @@ public class DefaultDrmSessionManager implements DrmSe if (session == null) { // Create a new session. - session = - new DefaultDrmSession<>( - uuid, - mediaDrm, - this, - schemeDatas, - mode, - offlineLicenseKeySetId, - optionalKeyRequestParameters, - callback, - playbackLooper, - eventDispatcher, - initialDrmRequestRetryCount); + session = createNewDefaultSession(schemeDatas, /* isPlaceholderSession= */ false); + if (!multiSession) { + noMultiSessionDrmSession = session; + } sessions.add(session); } session.acquire(); @@ -439,57 +538,64 @@ public class DefaultDrmSessionManager implements DrmSe } @Override - public void releaseSession(DrmSession session) { - if (session instanceof ErrorStateDrmSession) { - // Do nothing. - return; - } - - DefaultDrmSession drmSession = (DefaultDrmSession) session; - if (drmSession.release()) { - sessions.remove(drmSession); - if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) { - // Other sessions were waiting for the released session to complete a provision operation. - // We need to have one of those sessions perform the provision operation instead. - provisioningSessions.get(1).provision(); - } - provisioningSessions.remove(drmSession); - } - } - - // ProvisioningManager implementation. - - @Override - public void provisionRequired(DefaultDrmSession session) { - if (provisioningSessions.contains(session)) { - // The session has already requested provisioning. - return; - } - provisioningSessions.add(session); - if (provisioningSessions.size() == 1) { - // This is the first session requesting provisioning, so have it perform the operation. - session.provision(); - } - } - - @Override - public void onProvisionCompleted() { - for (DefaultDrmSession session : provisioningSessions) { - session.onProvisionCompleted(); - } - provisioningSessions.clear(); - } - - @Override - public void onProvisionError(Exception error) { - for (DefaultDrmSession session : provisioningSessions) { - session.onProvisionError(error); - } - provisioningSessions.clear(); + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return canAcquireSession(drmInitData) + ? Assertions.checkNotNull(exoMediaDrm).getExoMediaCryptoType() + : null; } // Internal methods. + private void assertExpectedPlaybackLooper(Looper playbackLooper) { + Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); + this.playbackLooper = playbackLooper; + } + + private void maybeCreateMediaDrmHandler(Looper playbackLooper) { + if (mediaDrmHandler == null) { + mediaDrmHandler = new MediaDrmHandler(playbackLooper); + } + } + + private DefaultDrmSession createNewDefaultSession( + @Nullable List schemeDatas, boolean isPlaceholderSession) { + Assertions.checkNotNull(exoMediaDrm); + // Placeholder sessions should always play clear samples without keys. + boolean playClearSamplesWithoutKeys = this.playClearSamplesWithoutKeys | isPlaceholderSession; + return new DefaultDrmSession<>( + uuid, + exoMediaDrm, + /* provisioningManager= */ provisioningManagerImpl, + /* releaseCallback= */ this::onSessionReleased, + schemeDatas, + mode, + playClearSamplesWithoutKeys, + isPlaceholderSession, + offlineLicenseKeySetId, + keyRequestParameters, + callback, + Assertions.checkNotNull(playbackLooper), + eventDispatcher, + loadErrorHandlingPolicy); + } + + private void onSessionReleased(DefaultDrmSession drmSession) { + sessions.remove(drmSession); + if (placeholderDrmSession == drmSession) { + placeholderDrmSession = null; + } + if (noMultiSessionDrmSession == drmSession) { + noMultiSessionDrmSession = null; + } + if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) { + // Other sessions were waiting for the released session to complete a provision operation. + // We need to have one of those sessions perform the provision operation instead. + provisioningSessions.get(1).provision(); + } + provisioningSessions.remove(drmSession); + } + /** * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}. * @@ -506,8 +612,9 @@ public class DefaultDrmSessionManager implements DrmSe List matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount); for (int i = 0; i < drmInitData.schemeDataCount; i++) { SchemeData schemeData = drmInitData.get(i); - boolean uuidMatches = schemeData.matches(uuid) - || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID)); + boolean uuidMatches = + schemeData.matches(uuid) + || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID)); if (uuidMatches && (schemeData.data != null || allowMissingData)) { matchingSchemeDatas.add(schemeData); } @@ -536,7 +643,37 @@ public class DefaultDrmSessionManager implements DrmSe } } } + } + private class ProvisioningManagerImpl implements DefaultDrmSession.ProvisioningManager { + @Override + public void provisionRequired(DefaultDrmSession session) { + if (provisioningSessions.contains(session)) { + // The session has already requested provisioning. + return; + } + provisioningSessions.add(session); + if (provisioningSessions.size() == 1) { + // This is the first session requesting provisioning, so have it perform the operation. + session.provision(); + } + } + + @Override + public void onProvisionCompleted() { + for (DefaultDrmSession session : provisioningSessions) { + session.onProvisionCompleted(); + } + provisioningSessions.clear(); + } + + @Override + public void onProvisionError(Exception error) { + for (DefaultDrmSession session : provisioningSessions) { + session.onProvisionError(error); + } + provisioningSessions.clear(); + } } private class MediaDrmEventListener implements OnEventListener { @@ -550,7 +687,5 @@ public class DefaultDrmSessionManager implements DrmSe @Nullable byte[] data) { Assertions.checkNotNull(mediaDrmHandler).obtainMessage(event, sessionId).sendToTarget(); } - } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 4fde9f05d..2f0246ba6 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.drm; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; @@ -87,7 +87,7 @@ public final class DrmInitData implements Comparator, Parcelable { private int hashCode; /** The protection scheme type, or null if not applicable or unknown. */ - public final @Nullable String schemeType; + @Nullable public final String schemeType; /** * Number of {@link SchemeData}s. @@ -152,7 +152,8 @@ public final class DrmInitData implements Comparator, Parcelable { * @return The initialization data for the scheme, or null if the scheme is not supported. */ @Deprecated - public @Nullable SchemeData get(UUID uuid) { + @Nullable + public SchemeData get(UUID uuid) { for (SchemeData schemeData : schemeDatas) { if (schemeData.matches(uuid)) { return schemeData; @@ -286,15 +287,11 @@ public final class DrmInitData implements Comparator, Parcelable { */ private final UUID uuid; /** The URL of the server to which license requests should be made. May be null if unknown. */ - public final @Nullable String licenseServerUrl; + @Nullable public final String licenseServerUrl; /** The mimeType of {@link #data}. */ public final String mimeType; /** The initialization data. May be null for scheme support checks only. */ - public final @Nullable byte[] data; - /** - * Whether secure decryption is required. - */ - public final boolean requiresSecureDecryption; + @Nullable public final byte[] data; /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is @@ -303,19 +300,7 @@ public final class DrmInitData implements Comparator, Parcelable { * @param data See {@link #data}. */ public SchemeData(UUID uuid, String mimeType, @Nullable byte[] data) { - this(uuid, mimeType, data, false); - } - - /** - * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is - * universal (i.e. applies to all schemes). - * @param mimeType See {@link #mimeType}. - * @param data See {@link #data}. - * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. - */ - public SchemeData( - UUID uuid, String mimeType, @Nullable byte[] data, boolean requiresSecureDecryption) { - this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption); + this(uuid, /* licenseServerUrl= */ null, mimeType, data); } /** @@ -324,19 +309,13 @@ public final class DrmInitData implements Comparator, Parcelable { * @param licenseServerUrl See {@link #licenseServerUrl}. * @param mimeType See {@link #mimeType}. * @param data See {@link #data}. - * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. */ public SchemeData( - UUID uuid, - @Nullable String licenseServerUrl, - String mimeType, - @Nullable byte[] data, - boolean requiresSecureDecryption) { + UUID uuid, @Nullable String licenseServerUrl, String mimeType, @Nullable byte[] data) { this.uuid = Assertions.checkNotNull(uuid); this.licenseServerUrl = licenseServerUrl; this.mimeType = Assertions.checkNotNull(mimeType); this.data = data; - this.requiresSecureDecryption = requiresSecureDecryption; } /* package */ SchemeData(Parcel in) { @@ -344,7 +323,6 @@ public final class DrmInitData implements Comparator, Parcelable { licenseServerUrl = in.readString(); mimeType = Util.castNonNull(in.readString()); data = in.createByteArray(); - requiresSecureDecryption = in.readByte() != 0; } /** @@ -381,7 +359,7 @@ public final class DrmInitData implements Comparator, Parcelable { * @return The new instance. */ public SchemeData copyWithData(@Nullable byte[] data) { - return new SchemeData(uuid, licenseServerUrl, mimeType, data, requiresSecureDecryption); + return new SchemeData(uuid, licenseServerUrl, mimeType, data); } @Override @@ -425,10 +403,8 @@ public final class DrmInitData implements Comparator, Parcelable { dest.writeString(licenseServerUrl); dest.writeString(mimeType); dest.writeByteArray(data); - dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); } - @SuppressWarnings("hiding") public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 392b0734b..35358f04f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.drm; import android.media.MediaDrm; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -29,9 +30,26 @@ import java.util.Map; public interface DrmSession { /** - * Wraps the throwable which is the cause of the error state. + * Invokes {@code newSession's} {@link #acquire()} and {@code previousSession's} {@link + * #release()} in that order. Null arguments are ignored. Does nothing if {@code previousSession} + * and {@code newSession} are the same session. */ - class DrmSessionException extends Exception { + static void replaceSession( + @Nullable DrmSession previousSession, @Nullable DrmSession newSession) { + if (previousSession == newSession) { + // Do nothing. + return; + } + if (newSession != null) { + newSession.acquire(); + } + if (previousSession != null) { + previousSession.release(); + } + } + + /** Wraps the throwable which is the cause of the error state. */ + class DrmSessionException extends IOException { public DrmSessionException(Throwable cause) { super(cause); @@ -59,13 +77,9 @@ public interface DrmSession { * The session is being opened. */ int STATE_OPENING = 2; - /** - * The session is open, but does not yet have the keys required for decryption. - */ + /** The session is open, but does not have keys required for decryption. */ int STATE_OPENED = 3; - /** - * The session is open and has the keys required for decryption. - */ + /** The session is open and has keys required for decryption. */ int STATE_OPENED_WITH_KEYS = 4; /** @@ -75,6 +89,11 @@ public interface DrmSession { */ @State int getState(); + /** Returns whether this session allows playback of clear samples prior to keys being loaded. */ + default boolean playClearSamplesWithoutKeys() { + return false; + } + /** * Returns the cause of the error state, or null if {@link #getState()} is not {@link * #STATE_ERROR}. @@ -110,4 +129,16 @@ public interface DrmSession { */ @Nullable byte[] getOfflineLicenseKeySetId(); + + /** + * Increments the reference count. When the caller no longer needs to use the instance, it must + * call {@link #release()} to decrement the reference count. + */ + void acquire(); + + /** + * Decrements the reference count. If the reference count drops to 0 underlying resources are + * released, and the instance cannot be re-used. + */ + void release(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index d8093507a..146c5d704 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.drm; import android.os.Looper; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; /** @@ -23,6 +25,51 @@ import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; */ public interface DrmSessionManager { + /** Returns {@link #DUMMY}. */ + @SuppressWarnings("unchecked") + static DrmSessionManager getDummyDrmSessionManager() { + return (DrmSessionManager) DUMMY; + } + + /** {@link DrmSessionManager} that supports no DRM schemes. */ + DrmSessionManager DUMMY = + new DrmSessionManager() { + + @Override + public boolean canAcquireSession(DrmInitData drmInitData) { + return false; + } + + @Override + public DrmSession acquireSession( + Looper playbackLooper, DrmInitData drmInitData) { + return new ErrorStateDrmSession<>( + new DrmSession.DrmSessionException( + new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME))); + } + + @Override + @Nullable + public Class getExoMediaCryptoType(DrmInitData drmInitData) { + return null; + } + }; + + /** + * Acquires any required resources. + * + *

    {@link #release()} must be called to ensure the acquired resources are released. After + * releasing, an instance may be re-prepared. + */ + default void prepare() { + // Do nothing. + } + + /** Releases any acquired resources. */ + default void release() { + // Do nothing. + } + /** * Returns whether the manager is capable of acquiring a session for the given * {@link DrmInitData}. @@ -34,8 +81,29 @@ public interface DrmSessionManager { boolean canAcquireSession(DrmInitData drmInitData); /** - * Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession} - * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required. + * Returns a {@link DrmSession} that does not execute key requests, with an incremented reference + * count. When the caller no longer needs to use the instance, it must call {@link + * DrmSession#release()} to decrement the reference count. + * + *

    Placeholder {@link DrmSession DrmSessions} may be used to configure secure decoders for + * playback of clear content periods. This can reduce the cost of transitioning between clear and + * encrypted content periods. + * + * @param playbackLooper The looper associated with the media playback thread. + * @param trackType The type of the track to acquire a placeholder session for. Must be one of the + * {@link C}{@code .TRACK_TYPE_*} constants. + * @return The placeholder DRM session, or null if this DRM session manager does not support + * placeholder sessions. + */ + @Nullable + default DrmSession acquirePlaceholderSession(Looper playbackLooper, int trackType) { + return null; + } + + /** + * Returns a {@link DrmSession} for the specified {@link DrmInitData}, with an incremented + * reference count. When the caller no longer needs to use the instance, it must call {@link + * DrmSession#release()} to decrement the reference count. * * @param playbackLooper The looper associated with the media playback thread. * @param drmInitData DRM initialization data. All contained {@link SchemeData}s must contain @@ -45,8 +113,9 @@ public interface DrmSessionManager { DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData); /** - * Releases a {@link DrmSession}. + * Returns the {@link ExoMediaCrypto} type returned by sessions acquired using the given {@link + * DrmInitData}, or null if a session cannot be acquired with the given {@link DrmInitData}. */ - void releaseSession(DrmSession drmSession); - + @Nullable + Class getExoMediaCryptoType(DrmInitData drmInitData); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java new file mode 100644 index 000000000..b619d9486 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/DummyExoMediaDrm.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.drm; + +import android.media.MediaDrmException; +import android.os.PersistableBundle; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.util.Util; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** An {@link ExoMediaDrm} that does not support any protection schemes. */ +@RequiresApi(18) +public final class DummyExoMediaDrm implements ExoMediaDrm { + + /** Returns a new instance. */ + @SuppressWarnings("unchecked") + public static DummyExoMediaDrm getInstance() { + return (DummyExoMediaDrm) new DummyExoMediaDrm<>(); + } + + @Override + public void setOnEventListener(OnEventListener listener) { + // Do nothing. + } + + @Override + public void setOnKeyStatusChangeListener(OnKeyStatusChangeListener listener) { + // Do nothing. + } + + @Override + public byte[] openSession() throws MediaDrmException { + throw new MediaDrmException("Attempting to open a session using a dummy ExoMediaDrm."); + } + + @Override + public void closeSession(byte[] sessionId) { + // Do nothing. + } + + @Override + public KeyRequest getKeyRequest( + byte[] scope, + @Nullable List schemeDatas, + int keyType, + @Nullable HashMap optionalParameters) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Nullable + @Override + public byte[] provideKeyResponse(byte[] scope, byte[] response) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + public ProvisionRequest getProvisionRequest() { + // Should not be invoked. No provision should be required. + throw new IllegalStateException(); + } + + @Override + public void provideProvisionResponse(byte[] response) { + // Should not be invoked. No provision should be required. + throw new IllegalStateException(); + } + + @Override + public Map queryKeyStatus(byte[] sessionId) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + public void acquire() { + // Do nothing. + } + + @Override + public void release() { + // Do nothing. + } + + @Override + public void restoreKeys(byte[] sessionId, byte[] keySetId) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + @Nullable + public PersistableBundle getMetrics() { + return null; + } + + @Override + public String getPropertyString(String propertyName) { + return ""; + } + + @Override + public byte[] getPropertyByteArray(String propertyName) { + return Util.EMPTY_BYTE_ARRAY; + } + + @Override + public void setPropertyString(String propertyName, String value) { + // Do nothing. + } + + @Override + public void setPropertyByteArray(String propertyName, byte[] value) { + // Do nothing. + } + + @Override + public T createMediaCrypto(byte[] sessionId) { + // Should not be invoked. No session should exist. + throw new IllegalStateException(); + } + + @Override + @Nullable + public Class getExoMediaCryptoType() { + // No ExoMediaCrypto type is supported. + return null; + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java index 82fd9a554..0028e4798 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -34,23 +34,41 @@ public final class ErrorStateDrmSession implements Drm } @Override - public @Nullable DrmSessionException getError() { + public boolean playClearSamplesWithoutKeys() { + return false; + } + + @Override + @Nullable + public DrmSessionException getError() { return error; } @Override - public @Nullable T getMediaCrypto() { + @Nullable + public T getMediaCrypto() { return null; } @Override - public @Nullable Map queryKeyStatus() { + @Nullable + public Map queryKeyStatus() { return null; } @Override - public @Nullable byte[] getOfflineLicenseKeySetId() { + @Nullable + public byte[] getOfflineLicenseKeySetId() { return null; } + @Override + public void acquire() { + // Do nothing. + } + + @Override + public void release() { + // Do nothing. + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 6bd8d9688..b6ee64484 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -21,6 +21,7 @@ import android.media.MediaDrm; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.os.Handler; +import android.os.PersistableBundle; import androidx.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import java.util.HashMap; @@ -30,12 +31,54 @@ import java.util.UUID; /** * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}. + * + *

    Reference counting

    + * + *

    Access to an instance is managed by reference counting, where {@link #acquire()} increments + * the reference count and {@link #release()} decrements it. When the reference count drops to 0 + * underlying resources are released, and the instance cannot be re-used. + * + *

    Each new instance has an initial reference count of 1. Hence application code that creates a + * new instance does not normally need to call {@link #acquire()}, and must call {@link #release()} + * when the instance is no longer required. */ public interface ExoMediaDrm { + /** {@link ExoMediaDrm} instances provider. */ + interface Provider { + + /** + * Returns an {@link ExoMediaDrm} instance with an incremented reference count. When the caller + * no longer needs to use the instance, it must call {@link ExoMediaDrm#release()} to decrement + * the reference count. + */ + ExoMediaDrm acquireExoMediaDrm(UUID uuid); + } + /** - * @see MediaDrm#EVENT_KEY_REQUIRED + * Provides an {@link ExoMediaDrm} instance owned by the app. + * + *

    Note that when using this provider the app will have instantiated the {@link ExoMediaDrm} + * instance, and remains responsible for calling {@link ExoMediaDrm#release()} on the instance + * when it's no longer being used. */ + final class AppManagedProvider implements Provider { + + private final ExoMediaDrm exoMediaDrm; + + /** Creates an instance that provides the given {@link ExoMediaDrm}. */ + public AppManagedProvider(ExoMediaDrm exoMediaDrm) { + this.exoMediaDrm = exoMediaDrm; + } + + @Override + public ExoMediaDrm acquireExoMediaDrm(UUID uuid) { + exoMediaDrm.acquire(); + return exoMediaDrm; + } + } + + /** @see MediaDrm#EVENT_KEY_REQUIRED */ @SuppressWarnings("InlinedApi") int EVENT_KEY_REQUIRED = MediaDrm.EVENT_KEY_REQUIRED; /** @@ -235,7 +278,17 @@ public interface ExoMediaDrm { Map queryKeyStatus(byte[] sessionId); /** - * @see MediaDrm#release() + * Increments the reference count. When the caller no longer needs to use the instance, it must + * call {@link #release()} to decrement the reference count. + * + *

    A new instance will have an initial reference count of 1, and therefore it is not normally + * necessary for application code to call this method. + */ + void acquire(); + + /** + * Decrements the reference count. If the reference count drops to 0 underlying resources are + * released, and the instance cannot be re-used. */ void release(); @@ -244,6 +297,14 @@ public interface ExoMediaDrm { */ void restoreKeys(byte[] sessionId, byte[] keySetId); + /** + * Returns drm metrics. May be null if unavailable. + * + * @see MediaDrm#getMetrics() + */ + @Nullable + PersistableBundle getMetrics(); + /** * @see MediaDrm#getPropertyString(String) */ @@ -271,4 +332,11 @@ public interface ExoMediaDrm { * @throws MediaCryptoException If the instance can't be created. */ T createMediaCrypto(byte[] sessionId) throws MediaCryptoException; + + /** + * Returns the {@link ExoMediaCrypto} type created by {@link #createMediaCrypto(byte[])}, or null + * if this instance cannot create any {@link ExoMediaCrypto} instances. + */ + @Nullable + Class getExoMediaCryptoType(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java index 7211b5fcd..c139b522e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.drm; import android.media.MediaCrypto; +import com.google.android.exoplayer2.util.Util; import java.util.UUID; /** @@ -24,6 +25,15 @@ import java.util.UUID; */ public final class FrameworkMediaCrypto implements ExoMediaCrypto { + /** + * Whether the device needs keys to have been loaded into the {@link DrmSession} before codec + * configuration. + */ + public static final boolean WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC = + "Amazon".equals(Util.MANUFACTURER) + && ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1 + || "AFTB".equals(Util.MODEL)); // Fire TV Gen 1 + /** The DRM scheme UUID. */ public final UUID uuid; /** The DRM session id. */ diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 2cb7e66a2..56d1aeea4 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -23,8 +23,10 @@ import android.media.MediaDrm; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; -import androidx.annotation.Nullable; +import android.os.PersistableBundle; import android.text.TextUtils; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; @@ -42,23 +44,40 @@ import java.util.List; import java.util.Map; import java.util.UUID; -/** - * An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. - */ +/** An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. */ @TargetApi(23) +@RequiresApi(18) public final class FrameworkMediaDrm implements ExoMediaDrm { + private static final String TAG = "FrameworkMediaDrm"; + + /** + * {@link ExoMediaDrm.Provider} that returns a new {@link FrameworkMediaDrm} for the requested + * UUID. Returns a {@link DummyExoMediaDrm} if the protection scheme identified by the given UUID + * is not supported by the device. + */ + public static final Provider DEFAULT_PROVIDER = + uuid -> { + try { + return newInstance(uuid); + } catch (UnsupportedDrmException e) { + Log.e(TAG, "Failed to instantiate a FrameworkMediaDrm for uuid: " + uuid + "."); + return new DummyExoMediaDrm<>(); + } + }; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private static final String MOCK_LA_URL_VALUE = "https://x"; private static final String MOCK_LA_URL = "" + MOCK_LA_URL_VALUE + ""; private static final int UTF_16_BYTES_PER_CHARACTER = 2; - private static final String TAG = "FrameworkMediaDrm"; private final UUID uuid; private final MediaDrm mediaDrm; + private int referenceCount; /** - * Creates an instance for the specified scheme UUID. + * Creates an instance with an initial reference count of 1. {@link #release()} must be called on + * the instance when it's no longer required. * * @param uuid The scheme uuid. * @return The created instance. @@ -79,6 +98,8 @@ public final class FrameworkMediaDrm implements ExoMediaDrm 0); + referenceCount++; + } + + @Override + public synchronized void release() { + if (--referenceCount == 0) { + mediaDrm.release(); + } } @Override @@ -195,6 +224,16 @@ public final class FrameworkMediaDrm implements ExoMediaDrm getExoMediaCryptoType() { + return FrameworkMediaCrypto.class; + } + private static SchemeData getSchemeData(UUID uuid, List schemeDatas) { if (!C.WIDEVINE_UUID.equals(uuid)) { // For non-Widevine CDMs always use the first scheme data. @@ -239,8 +283,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm mediaDrm, + ExoMediaDrm.Provider mediaDrmProvider, MediaDrmCallback callback, - @Nullable HashMap optionalKeyRequestParameters) { + @Nullable Map optionalKeyRequestParameters) { handlerThread = new HandlerThread("OfflineLicenseHelper"); handlerThread.start(); conditionVariable = new ConditionVariable(); @@ -147,39 +150,18 @@ public final class OfflineLicenseHelper { conditionVariable.open(); } }; + if (optionalKeyRequestParameters == null) { + optionalKeyRequestParameters = Collections.emptyMap(); + } drmSessionManager = - new DefaultDrmSessionManager<>(uuid, mediaDrm, callback, optionalKeyRequestParameters); + (DefaultDrmSessionManager) + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(uuid, mediaDrmProvider) + .setKeyRequestParameters(optionalKeyRequestParameters) + .build(callback); drmSessionManager.addListener(new Handler(handlerThread.getLooper()), eventListener); } - /** - * @see DefaultDrmSessionManager#getPropertyByteArray - */ - public synchronized byte[] getPropertyByteArray(String key) { - return drmSessionManager.getPropertyByteArray(key); - } - - /** - * @see DefaultDrmSessionManager#setPropertyByteArray - */ - public synchronized void setPropertyByteArray(String key, byte[] value) { - drmSessionManager.setPropertyByteArray(key, value); - } - - /** - * @see DefaultDrmSessionManager#getPropertyString - */ - public synchronized String getPropertyString(String key) { - return drmSessionManager.getPropertyString(key); - } - - /** - * @see DefaultDrmSessionManager#setPropertyString - */ - public synchronized void setPropertyString(String key, String value) { - drmSessionManager.setPropertyString(key, value); - } - /** * Downloads an offline license. * @@ -229,13 +211,15 @@ public final class OfflineLicenseHelper { public synchronized Pair getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId) throws DrmSessionException { Assertions.checkNotNull(offlineLicenseKeySetId); + drmSessionManager.prepare(); DrmSession drmSession = openBlockingKeyRequest( DefaultDrmSessionManager.MODE_QUERY, offlineLicenseKeySetId, DUMMY_DRM_INIT_DATA); DrmSessionException error = drmSession.getError(); Pair licenseDurationRemainingSec = WidevineUtil.getLicenseDurationRemainingSec(drmSession); - drmSessionManager.releaseSession(drmSession); + drmSession.release(); + drmSessionManager.release(); if (error != null) { if (error.getCause() instanceof KeysExpiredException) { return Pair.create(0L, 0L); @@ -255,11 +239,13 @@ public final class OfflineLicenseHelper { private byte[] blockingKeyRequest( @Mode int licenseMode, @Nullable byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { + drmSessionManager.prepare(); DrmSession drmSession = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId, drmInitData); DrmSessionException error = drmSession.getError(); byte[] keySetId = drmSession.getOfflineLicenseKeySetId(); - drmSessionManager.releaseSession(drmSession); + drmSession.release(); + drmSessionManager.release(); if (error != null) { throw error; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java index 9fed3b38e..004f873a3 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/WidevineUtil.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.drm; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import java.util.Map; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/package-info.java new file mode 100644 index 000000000..d4820dd20 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/drm/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.drm; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java old mode 100755 new mode 100644 index 220159bd1..1b30bf4a1 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -92,11 +92,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format) { + @FormatSupport + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { Assertions.checkNotNull(format.sampleMimeType); - if (!FfmpegLibrary.supportsFormat(format.sampleMimeType, format.pcmEncoding) - || !isOutputSupported(format)) { + if (!FfmpegLibrary.supportsFormat(format.sampleMimeType) || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; @@ -106,12 +106,13 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override + @AdaptiveSupport public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { return ADAPTIVE_NOT_SEAMLESS; } @Override - protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + protected FfmpegDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws FfmpegDecoderException { int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index e4e3b316e..1312b108e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.util.List; @@ -42,7 +43,7 @@ import java.util.List; private static final int DECODER_ERROR_OTHER = -2; private final String codecName; - private final @Nullable byte[] extraData; + @Nullable private final byte[] extraData; private final @C.Encoding int encoding; private final int outputBufferSize; @@ -60,9 +61,7 @@ import java.util.List; throws FfmpegDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); Assertions.checkNotNull(format.sampleMimeType); - codecName = - Assertions.checkNotNull( - FfmpegLibrary.getCodecName(format.sampleMimeType, format.pcmEncoding)); + codecName = Assertions.checkNotNull(FfmpegLibrary.getCodecName(format.sampleMimeType)); extraData = getExtraData(format.sampleMimeType, format.initializationData); encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; @@ -103,7 +102,7 @@ import java.util.List; return new FfmpegDecoderException("Error resetting (see logcat)."); } } - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Util.castNonNull(inputBuffer.data); int inputSize = inputData.limit(); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); @@ -129,8 +128,8 @@ import java.util.List; } hasOutputFormat = true; } - outputBuffer.data.position(0); - outputBuffer.data.limit(result); + outputData.position(0); + outputData.limit(result); return null; } @@ -141,16 +140,12 @@ import java.util.List; nativeContext = 0; } - /** - * Returns the channel count of output audio. May only be called after {@link #decode}. - */ + /** Returns the channel count of output audio. */ public int getChannelCount() { return channelCount; } - /** - * Returns the sample rate of output audio. May only be called after {@link #decode}. - */ + /** Returns the sample rate of output audio. */ public int getSampleRate() { return sampleRate; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index eb854bff4..a933bfdda 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.ext.ffmpeg; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.LibraryLoader; import com.google.android.exoplayer2.util.Log; @@ -44,10 +43,9 @@ public final class FfmpegLibrary { * Returns whether the underlying library supports the specified MIME type. * * @param mimeType The MIME type to check. - * @param encoding The PCM encoding for raw audio. */ - public static boolean supportsFormat(String mimeType, @C.PcmEncoding int encoding) { - String codecName = getCodecName(mimeType, encoding); + public static boolean supportsFormat(String mimeType) { + String codecName = getCodecName(mimeType); if (codecName == null) { return false; } @@ -62,7 +60,7 @@ public final class FfmpegLibrary { * Returns the name of the FFmpeg decoder that could be used to decode the format, or {@code null} * if it's unsupported. */ - /* package */ static @Nullable String getCodecName(String mimeType, @C.PcmEncoding int encoding) { + /* package */ static @Nullable String getCodecName(String mimeType) { switch (mimeType) { case MimeTypes.AUDIO_AAC: return "aac"; @@ -92,14 +90,10 @@ public final class FfmpegLibrary { return "flac"; case MimeTypes.AUDIO_ALAC: return "alac"; - case MimeTypes.AUDIO_RAW: - if (encoding == C.ENCODING_PCM_MU_LAW) { - return "pcm_mulaw"; - } else if (encoding == C.ENCODING_PCM_A_LAW) { - return "pcm_alaw"; - } else { - return null; - } + case MimeTypes.AUDIO_MLAW: + return "pcm_mulaw"; + case MimeTypes.AUDIO_ALAW: + return "pcm_alaw"; default: return null; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java new file mode 100644 index 000000000..a9fedb19c --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.ffmpeg; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java index 4bfcc003e..34b3ad2df 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacBinarySearchSeeker.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.extractor.BinarySearchSeeker; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; import com.google.android.exoplayer2.util.FlacStreamMetadata; import java.io.IOException; import java.nio.ByteBuffer; @@ -31,23 +32,50 @@ import java.nio.ByteBuffer; */ /* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker { + /** + * Holds a frame extracted from a stream, together with the time stamp of the frame in + * microseconds. + */ + public static final class OutputFrameHolder { + + public final ByteBuffer byteBuffer; + public long timeUs; + + /** Constructs an instance, wrapping the given byte buffer. */ + public OutputFrameHolder(ByteBuffer outputByteBuffer) { + this.timeUs = 0; + this.byteBuffer = outputByteBuffer; + } + } + private final FlacDecoderJni decoderJni; + /** + * Creates a {@link FlacBinarySearchSeeker}. + * + * @param streamMetadata The stream metadata. + * @param firstFramePosition The byte offset of the first frame in the stream. + * @param inputLength The length of the stream in bytes. + * @param decoderJni The FLAC JNI decoder. + * @param outputFrameHolder A holder used to retrieve the frame found by a seeking operation. + */ public FlacBinarySearchSeeker( FlacStreamMetadata streamMetadata, long firstFramePosition, long inputLength, - FlacDecoderJni decoderJni) { + FlacDecoderJni decoderJni, + OutputFrameHolder outputFrameHolder) { super( - new FlacSeekTimestampConverter(streamMetadata), - new FlacTimestampSeeker(decoderJni), - streamMetadata.durationUs(), + /* seekTimestampConverter= */ streamMetadata::getSampleNumber, + new FlacTimestampSeeker(decoderJni, outputFrameHolder), + streamMetadata.getDurationUs(), /* floorTimePosition= */ 0, /* ceilingTimePosition= */ streamMetadata.totalSamples, /* floorBytePosition= */ firstFramePosition, /* ceilingBytePosition= */ inputLength, /* approxBytesPerFrame= */ streamMetadata.getApproxBytesPerFrame(), - /* minimumSearchRange= */ Math.max(1, streamMetadata.minFrameSize)); + /* minimumSearchRange= */ Math.max( + FlacConstants.MIN_FRAME_HEADER_SIZE, streamMetadata.minFrameSize)); this.decoderJni = Assertions.checkNotNull(decoderJni); } @@ -63,14 +91,15 @@ import java.nio.ByteBuffer; private static final class FlacTimestampSeeker implements TimestampSeeker { private final FlacDecoderJni decoderJni; + private final OutputFrameHolder outputFrameHolder; - private FlacTimestampSeeker(FlacDecoderJni decoderJni) { + private FlacTimestampSeeker(FlacDecoderJni decoderJni, OutputFrameHolder outputFrameHolder) { this.decoderJni = decoderJni; + this.outputFrameHolder = outputFrameHolder; } @Override - public TimestampSearchResult searchForTimestamp( - ExtractorInput input, long targetSampleIndex, OutputFrameHolder outputFrameHolder) + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleIndex) throws IOException, InterruptedException { ByteBuffer outputBuffer = outputFrameHolder.byteBuffer; long searchPosition = input.getPosition(); @@ -97,6 +126,8 @@ import java.nio.ByteBuffer; if (targetSampleInLastFrame) { // We are holding the target frame in outputFrameHolder. Set its presentation time now. outputFrameHolder.timeUs = decoderJni.getLastFrameTimestamp(); + // The input position is passed even though it does not indicate the frame containing the + // target sample because the extractor must continue to read from this position. return TimestampSearchResult.targetFoundResult(input.getPosition()); } else if (nextFrameSampleIndex <= targetSampleIndex) { return TimestampSearchResult.underestimatedResult( @@ -106,21 +137,4 @@ import java.nio.ByteBuffer; } } } - - /** - * A {@link SeekTimestampConverter} implementation that returns the frame index (sample index) as - * the timestamp for a stream seek time position. - */ - private static final class FlacSeekTimestampConverter implements SeekTimestampConverter { - private final FlacStreamMetadata streamMetadata; - - public FlacSeekTimestampConverter(FlacStreamMetadata streamMetadata) { - this.streamMetadata = streamMetadata; - } - - @Override - public long timeUsToTargetTime(long timeUs) { - return Assertions.checkNotNull(streamMetadata).getSampleIndex(timeUs); - } - } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 50eb048d9..013b23ef2 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; @@ -32,7 +33,7 @@ import java.util.List; /* package */ final class FlacDecoder extends SimpleDecoder { - private final int maxOutputBufferSize; + private final FlacStreamMetadata streamMetadata; private final FlacDecoderJni decoderJni; /** @@ -58,7 +59,6 @@ import java.util.List; } decoderJni = new FlacDecoderJni(); decoderJni.setData(ByteBuffer.wrap(initializationData.get(0))); - FlacStreamMetadata streamMetadata; try { streamMetadata = decoderJni.decodeStreamMetadata(); } catch (ParserException e) { @@ -71,7 +71,6 @@ import java.util.List; int initialInputBufferSize = maxInputBufferSize != Format.NO_VALUE ? maxInputBufferSize : streamMetadata.maxFrameSize; setInitialInputBufferSize(initialInputBufferSize); - maxOutputBufferSize = streamMetadata.maxDecodedFrameSize(); } @Override @@ -101,8 +100,9 @@ import java.util.List; if (reset) { decoderJni.flush(); } - decoderJni.setData(inputBuffer.data); - ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, maxOutputBufferSize); + decoderJni.setData(Util.castNonNull(inputBuffer.data)); + ByteBuffer outputData = + outputBuffer.init(inputBuffer.timeUs, streamMetadata.getMaxDecodedFrameSize()); try { decoderJni.decodeSample(outputData); } catch (FlacDecoderJni.FlacFrameDecodeException e) { @@ -120,4 +120,8 @@ import java.util.List; decoderJni.release(); } + /** Returns the {@link FlacStreamMetadata} decoded from the initialization data. */ + public FlacStreamMetadata getStreamMetadata() { + return streamMetadata; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java index 9d0f5137e..f3e8551e1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderJni.java @@ -51,6 +51,12 @@ import java.nio.ByteBuffer; @Nullable private byte[] tempBuffer; private boolean endOfExtractorInput; + // the constructor does not initialize fields: tempBuffer + // call to flacInit() not allowed on the given receiver. + @SuppressWarnings({ + "nullness:initialization.fields.uninitialized", + "nullness:method.invocation.invalid" + }) public FlacDecoderJni() throws FlacDecoderException { nativeDecoderContext = flacInit(); if (nativeDecoderContext == 0) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 59fb7b483..2c6f51da0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -21,18 +21,17 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.extractor.BinarySearchSeeker.OutputFrameHolder; +import com.google.android.exoplayer2.ext.flac.FlacBinarySearchSeeker.OutputFrameHolder; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.Id3Peeker; +import com.google.android.exoplayer2.extractor.FlacMetadataReader; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; @@ -42,7 +41,6 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; -import java.util.Arrays; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -72,14 +70,7 @@ public final class FlacExtractor implements Extractor { */ public static final int FLAG_DISABLE_ID3_METADATA = 1; - /** - * FLAC signature: first 4 is the signature word, second 4 is the sizeof STREAMINFO. 0x22 is the - * mandatory STREAMINFO. - */ - private static final byte[] FLAC_SIGNATURE = {'f', 'L', 'a', 'C', 0, 0, 0, 0x22}; - private final ParsableByteArray outputBuffer; - private final Id3Peeker id3Peeker; private final boolean id3MetadataDisabled; @Nullable private FlacDecoderJni decoderJni; @@ -93,7 +84,7 @@ public final class FlacExtractor implements Extractor { @Nullable private Metadata id3Metadata; @Nullable private FlacBinarySearchSeeker binarySearchSeeker; - /** Constructs an instance with flags = 0. */ + /** Constructs an instance with {@code flags = 0}. */ public FlacExtractor() { this(/* flags= */ 0); } @@ -101,11 +92,11 @@ public final class FlacExtractor implements Extractor { /** * Constructs an instance. * - * @param flags Flags that control the extractor's behavior. + * @param flags Flags that control the extractor's behavior. Possible flags are described by + * {@link Flags}. */ public FlacExtractor(int flags) { outputBuffer = new ParsableByteArray(); - id3Peeker = new Id3Peeker(); id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; } @@ -123,17 +114,15 @@ public final class FlacExtractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { - if (input.getPosition() == 0) { - id3Metadata = peekId3Data(input); - } - return peekFlacSignature(input); + id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled); + return FlacMetadataReader.checkAndPeekStreamMarker(input); } @Override public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) { - id3Metadata = peekId3Data(input); + id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true); } FlacDecoderJni decoderJni = initDecoderJni(input); @@ -185,19 +174,6 @@ public final class FlacExtractor implements Extractor { } } - /** - * Peeks ID3 tag data at the beginning of the input. - * - * @return The first ID3 tag {@link Metadata}, or null if an ID3 tag is not present in the input. - */ - @Nullable - private Metadata peekId3Data(ExtractorInput input) throws IOException, InterruptedException { - input.resetPeekPosition(); - Id3Decoder.FramePredicate id3FramePredicate = - id3MetadataDisabled ? Id3Decoder.NO_FRAMES_PREDICATE : null; - return id3Peeker.peekId3Data(input, id3FramePredicate); - } - @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized. @SuppressWarnings({"contracts.postcondition.not.satisfied"}) private FlacDecoderJni initDecoderJni(ExtractorInput input) { @@ -214,11 +190,12 @@ public final class FlacExtractor implements Extractor { return; } + FlacDecoderJni flacDecoderJni = decoderJni; FlacStreamMetadata streamMetadata; try { - streamMetadata = decoderJni.decodeStreamMetadata(); + streamMetadata = flacDecoderJni.decodeStreamMetadata(); } catch (IOException e) { - decoderJni.reset(/* newPosition= */ 0); + flacDecoderJni.reset(/* newPosition= */ 0); input.setRetryPosition(/* position= */ 0, e); throw e; } @@ -226,15 +203,18 @@ public final class FlacExtractor implements Extractor { streamMetadataDecoded = true; if (this.streamMetadata == null) { this.streamMetadata = streamMetadata; - binarySearchSeeker = - outputSeekMap(decoderJni, streamMetadata, input.getLength(), extractorOutput); - Metadata metadata = id3MetadataDisabled ? null : id3Metadata; - if (streamMetadata.metadata != null) { - metadata = streamMetadata.metadata.copyWithAppendedEntriesFrom(metadata); - } - outputFormat(streamMetadata, metadata, trackOutput); - outputBuffer.reset(streamMetadata.maxDecodedFrameSize()); + outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize()); outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data)); + binarySearchSeeker = + outputSeekMap( + flacDecoderJni, + streamMetadata, + input.getLength(), + extractorOutput, + outputFrameHolder); + @Nullable + Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata); + outputFormat(streamMetadata, metadata, trackOutput); } } @@ -246,7 +226,7 @@ public final class FlacExtractor implements Extractor { OutputFrameHolder outputFrameHolder, TrackOutput trackOutput) throws InterruptedException, IOException { - int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition, outputFrameHolder); + int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition); ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer; if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) { outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput); @@ -254,18 +234,6 @@ public final class FlacExtractor implements Extractor { return seekResult; } - /** - * Peeks from the beginning of the input to see if {@link #FLAC_SIGNATURE} is present. - * - * @return Whether the input begins with {@link #FLAC_SIGNATURE}. - */ - private static boolean peekFlacSignature(ExtractorInput input) - throws IOException, InterruptedException { - byte[] header = new byte[FLAC_SIGNATURE.length]; - input.peekFully(header, /* offset= */ 0, FLAC_SIGNATURE.length); - return Arrays.equals(header, FLAC_SIGNATURE); - } - /** * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to * handle seeks. @@ -275,19 +243,21 @@ public final class FlacExtractor implements Extractor { FlacDecoderJni decoderJni, FlacStreamMetadata streamMetadata, long streamLength, - ExtractorOutput output) { + ExtractorOutput output, + OutputFrameHolder outputFrameHolder) { boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null; FlacBinarySearchSeeker binarySearchSeeker = null; SeekMap seekMap; if (haveSeekTable) { - seekMap = new FlacSeekMap(streamMetadata.durationUs(), decoderJni); + seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni); } else if (streamLength != C.LENGTH_UNSET) { long firstFramePosition = decoderJni.getDecodePosition(); binarySearchSeeker = - new FlacBinarySearchSeeker(streamMetadata, firstFramePosition, streamLength, decoderJni); + new FlacBinarySearchSeeker( + streamMetadata, firstFramePosition, streamLength, decoderJni, outputFrameHolder); seekMap = binarySearchSeeker.getSeekMap(); } else { - seekMap = new SeekMap.Unseekable(streamMetadata.durationUs()); + seekMap = new SeekMap.Unseekable(streamMetadata.getDurationUs()); } output.seekMap(seekMap); return binarySearchSeeker; @@ -300,8 +270,8 @@ public final class FlacExtractor implements Extractor { /* id= */ null, MimeTypes.AUDIO_RAW, /* codecs= */ null, - streamMetadata.bitRate(), - streamMetadata.maxDecodedFrameSize(), + streamMetadata.getBitRate(), + streamMetadata.getMaxDecodedFrameSize(), streamMetadata.channels, streamMetadata.sampleRate, getPcmEncoding(streamMetadata.bitsPerSample), diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index 30b5f0e9f..671eceea4 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -16,24 +16,31 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * Decodes and renders audio using the native Flac decoder. - */ -public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { +/** Decodes and renders audio using the native Flac decoder. */ +public final class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { private static final int NUM_BUFFERS = 16; + @MonotonicNonNull private FlacStreamMetadata streamMetadata; + public LibflacAudioRenderer() { - this(null, null); + this(/* eventHandler= */ null, /* eventListener= */ null); } /** @@ -43,18 +50,53 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibflacAudioRenderer( - Handler eventHandler, - AudioRendererEventListener eventListener, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { super(eventHandler, eventListener, audioProcessors); } + /** + * @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 audioSink The sink to which audio will be output. + */ + public LibflacAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { + super( + eventHandler, + eventListener, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + audioSink); + } + @Override - protected int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format) { + @FormatSupport + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { if (!MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { + } + // Compute the PCM encoding that the FLAC decoder will output. + @C.PcmEncoding int pcmEncoding; + if (format.initializationData.isEmpty()) { + // The initialization data might not be set if the format was obtained from a manifest (e.g. + // for DASH playbacks) rather than directly from the media. In this case we assume + // ENCODING_PCM_16BIT. If the actual encoding is different then playback will still succeed as + // long as the AudioSink supports it, which will always be true when using DefaultAudioSink. + pcmEncoding = C.ENCODING_PCM_16BIT; + } else { + int streamMetadataOffset = + FlacConstants.STREAM_MARKER_SIZE + FlacConstants.METADATA_BLOCK_HEADER_SIZE; + FlacStreamMetadata streamMetadata = + new FlacStreamMetadata(format.initializationData.get(0), streamMetadataOffset); + pcmEncoding = Util.getPcmEncoding(streamMetadata.bitsPerSample); + } + if (!supportsOutput(format.channelCount, pcmEncoding)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; @@ -64,10 +106,29 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + protected FlacDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws FlacDecoderException { - return new FlacDecoder( - NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); + FlacDecoder decoder = + new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.maxInputSize, format.initializationData); + streamMetadata = decoder.getStreamMetadata(); + return decoder; } + @Override + protected Format getOutputFormat() { + Assertions.checkNotNull(streamMetadata); + return Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + streamMetadata.channels, + streamMetadata.sampleRate, + Util.getPcmEncoding(streamMetadata.bitsPerSample), + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java new file mode 100644 index 000000000..ef6da7e3c --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/flac/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.flac; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 3991a6643..5f637c9a0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; @@ -23,22 +24,22 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.MimeTypes; -/** - * Decodes and renders audio using the native Opus decoder. - */ -public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { +/** Decodes and renders audio using the native Opus decoder. */ +public class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { /** The number of input and output buffers. */ private static final int NUM_BUFFERS = 16; /** The default input buffer size. */ private static final int DEFAULT_INPUT_BUFFER_SIZE = 960 * 6; - private OpusDecoder decoder; + private int channelCount; + private int sampleRate; public LibopusAudioRenderer() { - this(null, null); + this(/* eventHandler= */ null, /* eventListener= */ null); } /** @@ -48,8 +49,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibopusAudioRenderer( - Handler eventHandler, - AudioRendererEventListener eventListener, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { super(eventHandler, eventListener, audioProcessors); } @@ -66,22 +67,35 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output. + * @deprecated Use {@link #LibopusAudioRenderer(Handler, AudioRendererEventListener, + * AudioProcessor...)} instead, and pass DRM-related parameters to the {@link MediaSource} + * factories. */ - public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, - DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + @Deprecated + public LibopusAudioRenderer( + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, AudioProcessor... audioProcessors) { super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys, audioProcessors); } @Override - protected int supportsFormatInternal(DrmSessionManager drmSessionManager, - Format format) { + @FormatSupport + protected int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format) { + boolean drmIsSupported = + format.drmInitData == null + || OpusLibrary.matchesExpectedExoMediaCryptoType(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, format.drmInitData)); if (!MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; } else if (!supportsOutput(format.channelCount, C.ENCODING_PCM_16BIT)) { return FORMAT_UNSUPPORTED_SUBTYPE; - } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + } else if (!drmIsSupported) { return FORMAT_UNSUPPORTED_DRM; } else { return FORMAT_HANDLED; @@ -89,25 +103,36 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + protected OpusDecoder createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) throws OpusDecoderException { int initialInputBufferSize = format.maxInputSize != Format.NO_VALUE ? format.maxInputSize : DEFAULT_INPUT_BUFFER_SIZE; - decoder = + OpusDecoder decoder = new OpusDecoder( NUM_BUFFERS, NUM_BUFFERS, initialInputBufferSize, format.initializationData, mediaCrypto); + channelCount = decoder.getChannelCount(); + sampleRate = decoder.getSampleRate(); return decoder; } @Override protected Format getOutputFormat() { - return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT, - null, null, 0, null); + return Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + Format.NO_VALUE, + Format.NO_VALUE, + channelCount, + sampleRate, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index 051c41a8a..26f451ee2 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.drm.DecryptionException; import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.List; @@ -44,7 +45,7 @@ import java.util.List; private static final int DECODE_ERROR = -1; private static final int DRM_ERROR = -2; - private final ExoMediaCrypto exoMediaCrypto; + @Nullable private final ExoMediaCrypto exoMediaCrypto; private final int channelCount; private final int headerSkipSamples; @@ -66,8 +67,13 @@ import java.util.List; * content. Maybe null and can be ignored if decoder does not handle encrypted content. * @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder. */ - public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - List initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException { + public OpusDecoder( + int numInputBuffers, + int numOutputBuffers, + int initialInputBufferSize, + List initializationData, + @Nullable ExoMediaCrypto exoMediaCrypto) + throws OpusDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); this.exoMediaCrypto = exoMediaCrypto; if (exoMediaCrypto != null && !OpusLibrary.opusIsSecureDecodeSupported()) { @@ -81,8 +87,8 @@ import java.util.List; if (channelCount > 8) { throw new OpusDecoderException("Invalid channel count: " + channelCount); } - int preskip = readLittleEndian16(headerBytes, 10); - int gain = readLittleEndian16(headerBytes, 16); + int preskip = readUnsignedLittleEndian16(headerBytes, 10); + int gain = readSignedLittleEndian16(headerBytes, 16); byte[] streamMap = new byte[8]; int numStreams; @@ -157,7 +163,7 @@ import java.util.List; // any other time, skip number of samples as specified by seek preroll. skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples; } - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Util.castNonNull(inputBuffer.data); CryptoInfo cryptoInfo = inputBuffer.cryptoInfo; int result = inputBuffer.isEncrypted() ? opusSecureDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(), @@ -177,7 +183,7 @@ import java.util.List; } } - ByteBuffer outputData = outputBuffer.data; + ByteBuffer outputData = Util.castNonNull(outputBuffer.data); outputData.position(0); outputData.limit(result); if (skipSamples > 0) { @@ -219,20 +225,36 @@ import java.util.List; return (int) (ns * SAMPLE_RATE / 1000000000); } - private static int readLittleEndian16(byte[] input, int offset) { + private static int readUnsignedLittleEndian16(byte[] input, int offset) { int value = input[offset] & 0xFF; value |= (input[offset + 1] & 0xFF) << 8; return value; } + private static int readSignedLittleEndian16(byte[] input, int offset) { + return (short) readUnsignedLittleEndian16(input, offset); + } + private native long opusInit(int sampleRate, int channelCount, int numStreams, int numCoupled, int gain, byte[] streamMap); private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, SimpleOutputBuffer outputBuffer); - private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, - int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, - ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv, - int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); + + private native int opusSecureDecode( + long decoder, + long timeUs, + ByteBuffer inputBuffer, + int inputSize, + SimpleOutputBuffer outputBuffer, + int sampleRate, + @Nullable ExoMediaCrypto mediaCrypto, + int inputMode, + byte[] key, + byte[] iv, + int numSubSamples, + int[] numBytesOfClearData, + int[] numBytesOfEncryptedData); + private native void opusClose(long decoder); private native void opusReset(long decoder); private native int opusGetErrorCode(long decoder); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index 8e565b373..969dfe7cb 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -17,6 +17,9 @@ package com.google.android.exoplayer2.ext.opus; import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.LibraryLoader; +import com.google.android.exoplayer2.util.Util; /** * Configures and queries the underlying native library. @@ -27,14 +30,39 @@ public final class OpusLibrary { ExoPlayerLibraryInfo.registerModule("goog.exo.opus"); } + @Nullable private static Class exoMediaCryptoType; + private OpusLibrary() {} + /** + * Override the names of the Opus native libraries. If an application wishes to call this method, + * it must do so before calling any other method defined by this class, and before instantiating a + * {@link LibopusAudioRenderer} instance. + * + * @param exoMediaCryptoType The {@link ExoMediaCrypto} type expected for decoding protected + * content. + * @param libraries The names of the Opus native libraries. + */ + public static void setLibraries( + Class exoMediaCryptoType, String... libraries) { + OpusLibrary.exoMediaCryptoType = exoMediaCryptoType; + } + /** Returns the version of the underlying library if available, or null otherwise. */ @Nullable public static String getVersion() { return opusGetVersion(); } + /** + * Returns whether the given {@link ExoMediaCrypto} type matches the one required for decoding + * protected content. + */ + public static boolean matchesExpectedExoMediaCryptoType( + @Nullable Class exoMediaCryptoType) { + return Util.areEqual(OpusLibrary.exoMediaCryptoType, exoMediaCryptoType); + } + public static native String opusGetVersion(); public static native boolean opusIsSecureDecodeSupported(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java new file mode 100644 index 000000000..0848937fd --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/ext/opus/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.ext.opus; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java index 06d3ed603..0d823fa31 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.nio.ByteBuffer; /** * A seeker that supports seeking within a stream by searching for the target frame using binary @@ -48,38 +47,17 @@ public abstract class BinarySearchSeeker { * * @param input The {@link ExtractorInput} from which data should be peeked. * @param targetTimestamp The target timestamp. - * @param outputFrameHolder If {@link TimestampSearchResult#TYPE_TARGET_TIMESTAMP_FOUND} is - * returned, this holder may be updated to hold the extracted frame that contains the target - * frame/sample associated with the target timestamp. * @return A {@link TimestampSearchResult} that describes the result of the search. * @throws IOException If an error occurred reading from the input. * @throws InterruptedException If the thread was interrupted. */ - TimestampSearchResult searchForTimestamp( - ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) + TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) throws IOException, InterruptedException; /** Called when a seek operation finishes. */ default void onSeekFinished() {} } - /** - * Holds a frame extracted from a stream, together with the time stamp of the frame in - * microseconds. - */ - public static final class OutputFrameHolder { - - public final ByteBuffer byteBuffer; - - public long timeUs; - - /** Constructs an instance, wrapping the given byte buffer. */ - public OutputFrameHolder(ByteBuffer outputByteBuffer) { - this.timeUs = 0; - this.byteBuffer = outputByteBuffer; - } - } - /** * A {@link SeekTimestampConverter} implementation that returns the seek time itself as the * timestamp for a seek time position. @@ -189,15 +167,11 @@ public abstract class BinarySearchSeeker { * @param input The {@link ExtractorInput} from which data should be read. * @param seekPositionHolder If {@link Extractor#RESULT_SEEK} is returned, this holder is updated * to hold the position of the required seek. - * @param outputFrameHolder If {@link Extractor#RESULT_CONTINUE} is returned, this holder may be - * updated to hold the extracted frame that contains the target sample. The caller needs to - * check the byte buffer limit to see if an extracted frame is available. * @return One of the {@code RESULT_} values defined in {@link Extractor}. * @throws IOException If an error occurred reading from the input. * @throws InterruptedException If the thread was interrupted. */ - public int handlePendingSeek( - ExtractorInput input, PositionHolder seekPositionHolder, OutputFrameHolder outputFrameHolder) + public int handlePendingSeek(ExtractorInput input, PositionHolder seekPositionHolder) throws InterruptedException, IOException { TimestampSeeker timestampSeeker = Assertions.checkNotNull(this.timestampSeeker); while (true) { @@ -217,8 +191,7 @@ public abstract class BinarySearchSeeker { input.resetPeekPosition(); TimestampSearchResult timestampSearchResult = - timestampSeeker.searchForTimestamp( - input, seekOperationParams.getTargetTimePosition(), outputFrameHolder); + timestampSeeker.searchForTimestamp(input, seekOperationParams.getTargetTimePosition()); switch (timestampSearchResult.type) { case TimestampSearchResult.TYPE_POSITION_OVERESTIMATED: @@ -419,7 +392,7 @@ public abstract class BinarySearchSeeker { /** * Represents possible search results for {@link - * TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}. + * TimestampSeeker#searchForTimestamp(ExtractorInput, long)}. */ public static final class TimestampSearchResult { @@ -495,10 +468,6 @@ public abstract class BinarySearchSeeker { /** * Returns a result to signal that the target timestamp has been found at {@code * resultBytePosition}, and the seek operation can stop. - * - *

    Note that when this value is returned from {@link - * TimestampSeeker#searchForTimestamp(ExtractorInput, long, OutputFrameHolder)}, the {@link - * OutputFrameHolder} may be updated to hold the target frame as an optimization. */ public static TimestampSearchResult targetFoundResult(long resultBytePosition) { return new TimestampSearchResult( diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index 450cca42b..c6f1129da 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -58,7 +58,9 @@ public final class DefaultExtractorInput implements ExtractorInput { 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); + bytesRead = + readFromDataSource( + target, offset, length, /* bytesAlreadyRead= */ 0, /* allowEndOfInput= */ true); } commitBytesRead(bytesRead); return bytesRead; @@ -110,6 +112,31 @@ public final class DefaultExtractorInput implements ExtractorInput { skipFully(length, false); } + @Override + public int peek(byte[] target, int offset, int length) throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int peekBufferRemainingBytes = peekBufferLength - peekBufferPosition; + int bytesPeeked; + if (peekBufferRemainingBytes == 0) { + bytesPeeked = + readFromDataSource( + peekBuffer, + peekBufferPosition, + length, + /* bytesAlreadyRead= */ 0, + /* allowEndOfInput= */ true); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + return C.RESULT_END_OF_INPUT; + } + peekBufferLength += bytesPeeked; + } else { + bytesPeeked = Math.min(length, peekBufferRemainingBytes); + } + System.arraycopy(peekBuffer, peekBufferPosition, target, offset, bytesPeeked); + peekBufferPosition += bytesPeeked; + return bytesPeeked; + } + @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException, InterruptedException { @@ -201,7 +228,7 @@ public final class DefaultExtractorInput implements ExtractorInput { } /** - * Reads from the peek buffer + * 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. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 9522a7bb9..de3a48d97 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.extractor.amr.AmrExtractor; +import com.google.android.exoplayer2.extractor.flac.FlacExtractor; import com.google.android.exoplayer2.extractor.flv.FlvExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; @@ -50,20 +51,31 @@ import java.lang.reflect.Constructor; *

  • AC3 ({@link Ac3Extractor}) *
  • AC4 ({@link Ac4Extractor}) *
  • AMR ({@link AmrExtractor}) - *
  • FLAC (only available if the FLAC extension is built and included) + *
  • FLAC + *
      + *
    • If available, the FLAC extension extractor is used. + *
    • Otherwise, the core {@link FlacExtractor} is used. Note that Android devices do not + * generally include a FLAC decoder before API 27. This can be worked around by using + * the FLAC extension or the FFmpeg extension. + *
    *
*/ public final class DefaultExtractorsFactory implements ExtractorsFactory { - private static final Constructor FLAC_EXTRACTOR_CONSTRUCTOR; + private static final Constructor FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR; + static { - Constructor flacExtractorConstructor = null; + Constructor flacExtensionExtractorConstructor = null; try { // LINT.IfChange - flacExtractorConstructor = - Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") - .asSubclass(Extractor.class) - .getConstructor(); + @SuppressWarnings("nullness:argument.type.incompatible") + boolean isFlacNativeLibraryAvailable = true; + if (isFlacNativeLibraryAvailable) { + flacExtensionExtractorConstructor = + Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor") + .asSubclass(Extractor.class) + .getConstructor(); + } // LINT.ThenChange(../../../../../../../../proguard-rules.txt) } catch (ClassNotFoundException e) { // Expected if the app was built without the FLAC extension. @@ -71,7 +83,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { // The FLAC extension is present, but instantiation failed. throw new RuntimeException("Error instantiating FLAC extension", e); } - FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor; + FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR = flacExtensionExtractorConstructor; } private boolean constantBitrateSeekingEnabled; @@ -108,7 +120,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { /** * Sets flags for {@link AdtsExtractor} instances created by the factory. * - * @see AdtsExtractor#AdtsExtractor(long, int) + * @see AdtsExtractor#AdtsExtractor(int) * @param flags The flags to use. * @return The factory, for convenience. */ @@ -208,7 +220,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Override public synchronized Extractor[] createExtractors() { - Extractor[] extractors = new Extractor[FLAC_EXTRACTOR_CONSTRUCTOR == null ? 13 : 14]; + Extractor[] extractors = new Extractor[14]; extractors[0] = new MatroskaExtractor(matroskaFlags); extractors[1] = new FragmentedMp4Extractor(fragmentedMp4Flags); extractors[2] = new Mp4Extractor(mp4Flags); @@ -221,7 +233,6 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { : 0)); extractors[5] = new AdtsExtractor( - /* firstStreamSampleTimestampUs= */ 0, adtsFlags | (constantBitrateSeekingEnabled ? AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING @@ -238,13 +249,15 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { ? AmrExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING : 0)); extractors[12] = new Ac4Extractor(); - if (FLAC_EXTRACTOR_CONSTRUCTOR != null) { + if (FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR != null) { try { - extractors[13] = FLAC_EXTRACTOR_CONSTRUCTOR.newInstance(); + extractors[13] = FLAC_EXTENSION_EXTRACTOR_CONSTRUCTOR.newInstance(); } catch (Exception e) { // Should never happen. throw new IllegalStateException("Unexpected error creating FLAC extractor", e); } + } else { + extractors[13] = new FlacExtractor(); } return extractors; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index 083f31bcc..a9151a1b7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -89,6 +89,10 @@ public interface Extractor { * {@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. * + *

When this method throws an {@link IOException} or an {@link InterruptedException}, + * extraction may continue by providing an {@link ExtractorInput} with an unchanged {@link + * ExtractorInput#getPosition() read position} to a subsequent call to this method. + * * @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. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java index 45650c45f..8e5d6f044 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ExtractorInput.java @@ -18,9 +18,50 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; /** * Provides data to be consumed by an {@link Extractor}. + * + *

This interface provides two modes of accessing the underlying input. See the subheadings below + * for more info about each mode. + * + *

    + *
  • The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like + * byte-level access operations. + *
  • The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + *
+ * + *

{@link InputStream}-like methods

+ * + *

The {@code read()/peek()} and {@code skip()} methods provide {@link InputStream}-like + * byte-level access operations. The {@code length} parameter is a maximum, and each method returns + * the number of bytes actually processed. This may be less than {@code length} because the end of + * the input was reached, or the method was interrupted, or the operation was aborted early for + * another reason. + * + *

Block-based methods

+ * + *

The {@code read/skip/peekFully()} and {@code advancePeekPosition()} methods assume the user + * wants to read an entire block/frame/header of known length. + * + *

These methods all have a variant that takes a boolean {@code allowEndOfInput} parameter. This + * parameter is intended to be set to true when the caller believes the input might be fully + * exhausted before the call is made (i.e. they've previously read/skipped/peeked the final + * block/frame/header). It's not intended to allow a partial read (i.e. greater than 0 bytes, + * but less than {@code length}) to succeed - this will always throw an {@link EOFException} from + * these methods (a partial read is assumed to indicate a malformed block/frame/header - and + * therefore a malformed file). + * + *

The expected behaviour of the block-based methods is therefore: + * + *

    + *
  • Already at end-of-input and {@code allowEndOfInput=false}: Throw {@link EOFException}. + *
  • Already at end-of-input and {@code allowEndOfInput=true}: Return {@code false}. + *
  • Encounter end-of-input during read/skip/peek/advance: Throw {@link EOFException} + * (regardless of {@code allowEndOfInput}). + *
*/ public interface ExtractorInput { @@ -41,22 +82,16 @@ public interface ExtractorInput { /** * 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. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the read was successful. False if {@code allowEndOfInput=true} and 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. @@ -67,7 +102,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Equivalent to {@code readFully(target, offset, length, false)}. + * Equivalent to {@link #readFully(byte[], int, int, boolean) 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. @@ -94,9 +130,10 @@ public interface ExtractorInput { * @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. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the skip was successful. False if {@code allowEndOfInput=true} and 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. @@ -119,25 +156,37 @@ public interface ExtractorInput { 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 + * Peeks up to {@code length} bytes from the peek position. The current read position is left + * unchanged. + * + *

This method blocks until at least one byte of data can be peeked, the end of the input is + * detected, or an exception 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 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 maximum number of bytes to peek from the input. + * @return The number of bytes peeked, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int peek(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #peek(byte[], int, int)}, but peeks the requested {@code length} in full. + * + * @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. + * considered an error, causing an {@link EOFException} to be thrown. See note in class + * Javadoc. + * @return True if the peek was successful. False if {@code allowEndOfInput=true} and 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. @@ -148,12 +197,8 @@ public interface ExtractorInput { 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 and skipping also reset the peek - * position. + * Equivalent to {@link #peekFully(byte[], int, int, boolean) peekFully(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. @@ -165,18 +210,16 @@ public interface ExtractorInput { 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. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int, + * boolean)} except the data is skipped instead of read. * * @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. + * error, causing an {@link EOFException} to be thrown. See note in class Javadoc. + * @return True if advancing the peek position was successful. False if {@code + * allowEndOfInput=true} and the end of the input was encountered before advancing over any + * data. * @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. @@ -187,7 +230,8 @@ public interface ExtractorInput { throws IOException, InterruptedException; /** - * Advances the peek position by {@code length} bytes. + * Advances the peek position by {@code length} bytes. Like {@link #peekFully(byte[], int, int)} + * except the data is skipped instead of read. * * @param length The number of bytes to peek from the input. * @throws EOFException If the end of input was encountered. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ExtractorUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ExtractorUtil.java new file mode 100644 index 000000000..3867a0fde --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ExtractorUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import com.google.android.exoplayer2.C; +import java.io.IOException; + +/** Extractor related utility methods. */ +/* package */ final class ExtractorUtil { + + /** + * Peeks {@code length} bytes from the input peek position, or all the bytes to the end of the + * input if there was less than {@code length} bytes left. + * + *

If an exception is thrown, there is no guarantee on the peek position. + * + * @param input The stream input to peek the data from. + * @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 peek from the input. + * @return The number of bytes peeked. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static int peekToLength(ExtractorInput input, byte[] target, int offset, int length) + throws IOException, InterruptedException { + int totalBytesPeeked = 0; + while (totalBytesPeeked < length) { + int bytesPeeked = input.peek(target, offset + totalBytesPeeked, length - totalBytesPeeked); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + break; + } + totalBytesPeeked += bytesPeeked; + } + return totalBytesPeeked; + } + + private ExtractorUtil() {} +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java new file mode 100644 index 000000000..f014eaa56 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacFrameReader.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * Reads and peeks FLAC frame elements according to the FLAC format specification. + */ +public final class FlacFrameReader { + + /** Holds a sample number. */ + public static final class SampleNumberHolder { + /** The sample number. */ + public long sampleNumber; + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, reads it and writes the frame + * first sample number in {@code sampleNumberHolder}. + * + *

If the header is valid, the position of {@code data} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the frame header is valid. + */ + public static boolean checkAndReadFrameHeader( + ParsableByteArray data, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + SampleNumberHolder sampleNumberHolder) { + int frameStartPosition = data.getPosition(); + + long frameHeaderBytes = data.readUnsignedInt(); + if (frameHeaderBytes >>> 16 != frameStartMarker) { + return false; + } + + boolean isBlockSizeVariable = (frameHeaderBytes >>> 16 & 1) == 1; + int blockSizeKey = (int) (frameHeaderBytes >> 12 & 0xF); + int sampleRateKey = (int) (frameHeaderBytes >> 8 & 0xF); + int channelAssignmentKey = (int) (frameHeaderBytes >> 4 & 0xF); + int bitsPerSampleKey = (int) (frameHeaderBytes >> 1 & 0x7); + boolean reservedBit = (frameHeaderBytes & 1) == 1; + return checkChannelAssignment(channelAssignmentKey, flacStreamMetadata) + && checkBitsPerSample(bitsPerSampleKey, flacStreamMetadata) + && !reservedBit + && checkAndReadFirstSampleNumber( + data, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder) + && checkAndReadBlockSizeSamples(data, flacStreamMetadata, blockSizeKey) + && checkAndReadSampleRate(data, flacStreamMetadata, sampleRateKey) + && checkAndReadCrc(data, frameStartPosition); + } + + /** + * Checks whether the given FLAC frame header is valid and, if so, writes the frame first sample + * number in {@code sampleNumberHolder}. + * + *

The {@code input} peek position is left unchanged. + * + * @param input The input to get the data from, whose peek position must correspond to the frame + * header. + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker of the stream. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the frame header is valid. + */ + public static boolean checkFrameHeaderFromPeek( + ExtractorInput input, + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + SampleNumberHolder sampleNumberHolder) + throws IOException, InterruptedException { + long originalPeekPosition = input.getPeekPosition(); + + byte[] frameStartBytes = new byte[2]; + input.peekFully(frameStartBytes, 0, 2); + int frameStart = (frameStartBytes[0] & 0xFF) << 8 | (frameStartBytes[1] & 0xFF); + if (frameStart != frameStartMarker) { + input.resetPeekPosition(); + input.advancePeekPosition((int) (originalPeekPosition - input.getPosition())); + return false; + } + + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.MAX_FRAME_HEADER_SIZE); + System.arraycopy( + frameStartBytes, /* srcPos= */ 0, scratch.data, /* destPos= */ 0, /* length= */ 2); + + int totalBytesPeeked = + ExtractorUtil.peekToLength(input, scratch.data, 2, FlacConstants.MAX_FRAME_HEADER_SIZE - 2); + scratch.setLimit(totalBytesPeeked); + + input.resetPeekPosition(); + input.advancePeekPosition((int) (originalPeekPosition - input.getPosition())); + + return checkAndReadFrameHeader( + scratch, flacStreamMetadata, frameStartMarker, sampleNumberHolder); + } + + /** + * Returns the number of the first sample in the given frame. + * + *

The read position of {@code input} is left unchanged. + * + *

If no exception is thrown, the peek position is aligned with the read position. Otherwise, + * there is no guarantee on the peek position. + * + * @param input Input stream to get the sample number from (starting from the read position). + * @return The frame first sample number. + * @throws ParserException If an error occurs parsing the sample number. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + */ + public static long getFirstSampleNumber( + ExtractorInput input, FlacStreamMetadata flacStreamMetadata) + throws IOException, InterruptedException { + input.resetPeekPosition(); + input.advancePeekPosition(1); + byte[] blockingStrategyByte = new byte[1]; + input.peekFully(blockingStrategyByte, 0, 1); + boolean isBlockSizeVariable = (blockingStrategyByte[0] & 1) == 1; + input.advancePeekPosition(2); + + int maxUtf8SampleNumberSize = isBlockSizeVariable ? 7 : 6; + ParsableByteArray scratch = new ParsableByteArray(maxUtf8SampleNumberSize); + int totalBytesPeeked = + ExtractorUtil.peekToLength(input, scratch.data, 0, maxUtf8SampleNumberSize); + scratch.setLimit(totalBytesPeeked); + input.resetPeekPosition(); + + SampleNumberHolder sampleNumberHolder = new SampleNumberHolder(); + if (!checkAndReadFirstSampleNumber( + scratch, flacStreamMetadata, isBlockSizeVariable, sampleNumberHolder)) { + throw new ParserException(); + } + + return sampleNumberHolder.sampleNumber; + } + + /** + * Reads the given block size. + * + * @param data The array to read the data from, whose position must correspond to the block size + * bits. + * @param blockSizeKey The key in the block size lookup table. + * @return The block size in samples, or -1 if the {@code blockSizeKey} is invalid. + */ + public static int readFrameBlockSizeSamplesFromKey(ParsableByteArray data, int blockSizeKey) { + switch (blockSizeKey) { + case 1: + return 192; + case 2: + case 3: + case 4: + case 5: + return 576 << (blockSizeKey - 2); + case 6: + return data.readUnsignedByte() + 1; + case 7: + return data.readUnsignedShort() + 1; + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + return 256 << (blockSizeKey - 8); + default: + return -1; + } + } + + /** + * Checks whether the given channel assignment is valid. + * + * @param channelAssignmentKey The channel assignment lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the channel assignment is valid. + */ + private static boolean checkChannelAssignment( + int channelAssignmentKey, FlacStreamMetadata flacStreamMetadata) { + if (channelAssignmentKey <= 7) { + return channelAssignmentKey == flacStreamMetadata.channels - 1; + } else if (channelAssignmentKey <= 10) { + return flacStreamMetadata.channels == 2; + } else { + return false; + } + } + + /** + * Checks whether the given number of bits per sample is valid. + * + * @param bitsPerSampleKey The bits per sample lookup key. + * @param flacStreamMetadata The stream metadata. + * @return Whether the number of bits per sample is valid. + */ + private static boolean checkBitsPerSample( + int bitsPerSampleKey, FlacStreamMetadata flacStreamMetadata) { + if (bitsPerSampleKey == 0) { + return true; + } + return bitsPerSampleKey == flacStreamMetadata.bitsPerSampleLookupKey; + } + + /** + * Checks whether the given sample number is valid and, if so, reads it and writes it in {@code + * sampleNumberHolder}. + * + *

If the sample number is valid, the position of {@code data} is moved to the byte following + * it. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the sample + * number data. + * @param flacStreamMetadata The stream metadata. + * @param isBlockSizeVariable Whether the stream blocking strategy is variable block size or fixed + * block size. + * @param sampleNumberHolder The holder used to contain the sample number. + * @return Whether the sample number is valid. + */ + private static boolean checkAndReadFirstSampleNumber( + ParsableByteArray data, + FlacStreamMetadata flacStreamMetadata, + boolean isBlockSizeVariable, + SampleNumberHolder sampleNumberHolder) { + long utf8Value; + try { + utf8Value = data.readUtf8EncodedLong(); + } catch (NumberFormatException e) { + return false; + } + + sampleNumberHolder.sampleNumber = + isBlockSizeVariable ? utf8Value : utf8Value * flacStreamMetadata.maxBlockSizeSamples; + return true; + } + + /** + * Checks whether the given frame block size key and block size bits are valid and, if so, reads + * the block size bits. + * + *

If the block size is valid, the position of {@code data} is moved to the byte following the + * block size bits. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must correspond to the block size + * bits. + * @param flacStreamMetadata The stream metadata. + * @param blockSizeKey The key in the block size lookup table. + * @return Whether the block size is valid. + */ + private static boolean checkAndReadBlockSizeSamples( + ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int blockSizeKey) { + int blockSizeSamples = readFrameBlockSizeSamplesFromKey(data, blockSizeKey); + return blockSizeSamples != -1 && blockSizeSamples <= flacStreamMetadata.maxBlockSizeSamples; + } + + /** + * Checks whether the given sample rate key and sample rate bits are valid and, if so, reads the + * sample rate bits. + * + *

If the sample rate is valid, the position of {@code data} is moved to the byte following the + * sample rate bits. Otherwise, there is no guarantee on the position. + * + * @param data The array to read the data from, whose position must indicate the sample rate bits. + * @param flacStreamMetadata The stream metadata. + * @param sampleRateKey The key in the sample rate lookup table. + * @return Whether the sample rate is valid. + */ + private static boolean checkAndReadSampleRate( + ParsableByteArray data, FlacStreamMetadata flacStreamMetadata, int sampleRateKey) { + int expectedSampleRate = flacStreamMetadata.sampleRate; + if (sampleRateKey == 0) { + return true; + } else if (sampleRateKey <= 11) { + return sampleRateKey == flacStreamMetadata.sampleRateLookupKey; + } else if (sampleRateKey == 12) { + return data.readUnsignedByte() * 1000 == expectedSampleRate; + } else if (sampleRateKey <= 14) { + int sampleRate = data.readUnsignedShort(); + if (sampleRateKey == 14) { + sampleRate *= 10; + } + return sampleRate == expectedSampleRate; + } else { + return false; + } + } + + /** + * Checks whether the given CRC is valid and, if so, reads it. + * + *

If the CRC is valid, the position of {@code data} is moved to the byte following it. + * Otherwise, there is no guarantee on the position. + * + *

The {@code data} array must contain the whole frame header. + * + * @param data The array to read the data from, whose position must indicate the CRC. + * @param frameStartPosition The frame start offset in {@code data}. + * @return Whether the CRC is valid. + */ + private static boolean checkAndReadCrc(ParsableByteArray data, int frameStartPosition) { + int crc = data.readUnsignedByte(); + int frameEndPosition = data.getPosition(); + int expectedCrc = + Util.crc8(data.data, frameStartPosition, frameEndPosition - 1, /* initialValue= */ 0); + return crc == expectedCrc; + } + + private FlacFrameReader() {} +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java new file mode 100644 index 000000000..49d4558dd --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacMetadataReader.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.flac.PictureFrame; +import com.google.android.exoplayer2.metadata.id3.Id3Decoder; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Reads and peeks FLAC stream metadata elements according to the FLAC format specification. + */ +public final class FlacMetadataReader { + + /** Holds a {@link FlacStreamMetadata}. */ + public static final class FlacStreamMetadataHolder { + /** The FLAC stream metadata. */ + @Nullable public FlacStreamMetadata flacStreamMetadata; + + public FlacStreamMetadataHolder(@Nullable FlacStreamMetadata flacStreamMetadata) { + this.flacStreamMetadata = flacStreamMetadata; + } + } + + private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC" + private static final int SYNC_CODE = 0x3FFE; + private static final int SEEK_POINT_SIZE = 18; + + /** + * Peeks ID3 Data. + * + * @param input Input stream to peek the ID3 data from. + * @param parseData Whether to parse the ID3 frames. + * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData} + * is {@code false}. + * @throws IOException If peeking from the input fails. In this case, there is no guarantee on the + * peek position. + * @throws InterruptedException If interrupted while peeking from input. In this case, there is no + * guarantee on the peek position. + */ + @Nullable + public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData) + throws IOException, InterruptedException { + @Nullable + Id3Decoder.FramePredicate id3FramePredicate = parseData ? null : Id3Decoder.NO_FRAMES_PREDICATE; + @Nullable Metadata id3Metadata = new Id3Peeker().peekId3Data(input, id3FramePredicate); + return id3Metadata == null || id3Metadata.length() == 0 ? null : id3Metadata; + } + + /** + * Peeks the FLAC stream marker. + * + * @param input Input stream to peek the stream marker from. + * @return Whether the data peeked is the FLAC stream marker. + * @throws IOException If peeking from the input fails. In this case, the peek position is left + * unchanged. + * @throws InterruptedException If interrupted while peeking from input. In this case, the peek + * position is left unchanged. + */ + public static boolean checkAndPeekStreamMarker(ExtractorInput input) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); + input.peekFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE); + return scratch.readUnsignedInt() == STREAM_MARKER; + } + + /** + * Reads ID3 Data. + * + *

If no exception is thrown, the peek position of {@code input} is aligned with the read + * position. + * + * @param input Input stream to read the ID3 data from. + * @param parseData Whether to parse the ID3 frames. + * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData} + * is {@code false}. + * @throws IOException If reading from the input fails. In this case, the read position is left + * unchanged and there is no guarantee on the peek position. + * @throws InterruptedException If interrupted while reading from input. In this case, the read + * position is left unchanged and there is no guarantee on the peek position. + */ + @Nullable + public static Metadata readId3Metadata(ExtractorInput input, boolean parseData) + throws IOException, InterruptedException { + input.resetPeekPosition(); + long startingPeekPosition = input.getPeekPosition(); + @Nullable Metadata id3Metadata = peekId3Metadata(input, parseData); + int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition); + input.skipFully(peekedId3Bytes); + return id3Metadata; + } + + /** + * Reads the FLAC stream marker. + * + * @param input Input stream to read the stream marker from. + * @throws ParserException If an error occurs parsing the stream marker. In this case, the + * position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes. + * @throws IOException If reading from the input fails. In this case, the position is left + * unchanged. + * @throws InterruptedException If interrupted while reading from input. In this case, the + * position is left unchanged. + */ + public static void readStreamMarker(ExtractorInput input) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE); + input.readFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE); + if (scratch.readUnsignedInt() != STREAM_MARKER) { + throw new ParserException("Failed to read FLAC stream marker."); + } + } + + /** + * Reads one FLAC metadata block. + * + *

If no exception is thrown, the peek position of {@code input} is aligned with the read + * position. + * + * @param input Input stream to read the metadata block from (header included). + * @param metadataHolder A holder for the metadata read. If the stream info block (which must be + * the first metadata block) is read, the holder contains a new instance representing the + * stream info data. If the block read is a Vorbis comment block or a picture block, the + * holder contains a copy of the existing stream metadata with the corresponding metadata + * added. Otherwise, the metadata in the holder is unchanged. + * @return Whether the block read is the last metadata block. + * @throws IllegalArgumentException If the block read is not a stream info block and the metadata + * in {@code metadataHolder} is {@code null}. In this case, the read position will be at the + * start of a metadata block and there is no guarantee on the peek position. + * @throws IOException If reading from the input fails. In this case, the read position will be at + * the start of a metadata block and there is no guarantee on the peek position. + * @throws InterruptedException If interrupted while reading from input. In this case, the read + * position will be at the start of a metadata block and there is no guarantee on the peek + * position. + */ + public static boolean readMetadataBlock( + ExtractorInput input, FlacStreamMetadataHolder metadataHolder) + throws IOException, InterruptedException { + input.resetPeekPosition(); + ParsableBitArray scratch = new ParsableBitArray(new byte[4]); + input.peekFully(scratch.data, 0, FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + boolean isLastMetadataBlock = scratch.readBit(); + int type = scratch.readBits(7); + int length = FlacConstants.METADATA_BLOCK_HEADER_SIZE + scratch.readBits(24); + if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) { + metadataHolder.flacStreamMetadata = readStreamInfoBlock(input); + } else { + FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata; + if (flacStreamMetadata == null) { + throw new IllegalArgumentException(); + } + if (type == FlacConstants.METADATA_TYPE_SEEK_TABLE) { + FlacStreamMetadata.SeekTable seekTable = readSeekTableMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithSeekTable(seekTable); + } else if (type == FlacConstants.METADATA_TYPE_VORBIS_COMMENT) { + List vorbisComments = readVorbisCommentMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = + flacStreamMetadata.copyWithVorbisComments(vorbisComments); + } else if (type == FlacConstants.METADATA_TYPE_PICTURE) { + PictureFrame pictureFrame = readPictureMetadataBlock(input, length); + metadataHolder.flacStreamMetadata = + flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame)); + } else { + input.skipFully(length); + } + } + + return isLastMetadataBlock; + } + + /** + * Reads a FLAC seek table metadata block. + * + *

The position of {@code data} is moved to the byte following the seek table metadata block + * (placeholder points included). + * + * @param data The array to read the data from, whose position must correspond to the seek table + * metadata block (header included). + * @return The seek table, without the placeholder points. + */ + public static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(ParsableByteArray data) { + data.skipBytes(1); + int length = data.readUnsignedInt24(); + + long seekTableEndPosition = data.getPosition() + length; + int seekPointCount = length / SEEK_POINT_SIZE; + long[] pointSampleNumbers = new long[seekPointCount]; + long[] pointOffsets = new long[seekPointCount]; + for (int i = 0; i < seekPointCount; i++) { + // The sample number is expected to fit in a signed long, except if it is a placeholder, in + // which case its value is -1. + long sampleNumber = data.readLong(); + if (sampleNumber == -1) { + pointSampleNumbers = Arrays.copyOf(pointSampleNumbers, i); + pointOffsets = Arrays.copyOf(pointOffsets, i); + break; + } + pointSampleNumbers[i] = sampleNumber; + pointOffsets[i] = data.readLong(); + data.skipBytes(2); + } + + data.skipBytes((int) (seekTableEndPosition - data.getPosition())); + return new FlacStreamMetadata.SeekTable(pointSampleNumbers, pointOffsets); + } + + /** + * Returns the frame start marker, consisting of the 2 first bytes of the first frame. + * + *

The read position of {@code input} is left unchanged and the peek position is aligned with + * the read position. + * + * @param input Input stream to get the start marker from (starting from the read position). + * @return The frame start marker (which must be the same for all the frames in the stream). + * @throws ParserException If an error occurs parsing the frame start marker. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + */ + public static int getFrameStartMarker(ExtractorInput input) + throws IOException, InterruptedException { + input.resetPeekPosition(); + ParsableByteArray scratch = new ParsableByteArray(2); + input.peekFully(scratch.data, 0, 2); + + int frameStartMarker = scratch.readUnsignedShort(); + int syncCode = frameStartMarker >> 2; + if (syncCode != SYNC_CODE) { + input.resetPeekPosition(); + throw new ParserException("First frame does not start with sync code."); + } + + input.resetPeekPosition(); + return frameStartMarker; + } + + private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input) + throws IOException, InterruptedException { + byte[] scratchData = new byte[FlacConstants.STREAM_INFO_BLOCK_SIZE]; + input.readFully(scratchData, 0, FlacConstants.STREAM_INFO_BLOCK_SIZE); + return new FlacStreamMetadata( + scratchData, /* offset= */ FlacConstants.METADATA_BLOCK_HEADER_SIZE); + } + + private static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock( + ExtractorInput input, int length) throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + return readSeekTableMetadataBlock(scratch); + } + + private static List readVorbisCommentMetadataBlock(ExtractorInput input, int length) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE); + CommentHeader commentHeader = + VorbisUtil.readVorbisCommentHeader( + scratch, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false); + return Arrays.asList(commentHeader.comments); + } + + private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(length); + input.readFully(scratch.data, 0, length); + scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE); + + int pictureType = scratch.readInt(); + int mimeTypeLength = scratch.readInt(); + String mimeType = scratch.readString(mimeTypeLength, Charset.forName(C.ASCII_NAME)); + int descriptionLength = scratch.readInt(); + String description = scratch.readString(descriptionLength); + int width = scratch.readInt(); + int height = scratch.readInt(); + int depth = scratch.readInt(); + int colors = scratch.readInt(); + int pictureDataLength = scratch.readInt(); + byte[] pictureData = new byte[pictureDataLength]; + scratch.readBytes(pictureData, 0, pictureDataLength); + + return new PictureFrame( + pictureType, mimeType, description, width, height, depth, colors, pictureData); + } + + private FlacMetadataReader() {} +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java new file mode 100644 index 000000000..a711f09e2 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/FlacSeekTableSeekMap.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.Util; + +/** + * A {@link SeekMap} implementation for FLAC streams that contain a seek table. + */ +public final class FlacSeekTableSeekMap implements SeekMap { + + private final FlacStreamMetadata flacStreamMetadata; + private final long firstFrameOffset; + + /** + * Creates a seek map from the FLAC stream seek table. + * + * @param flacStreamMetadata The stream metadata. + * @param firstFrameOffset The byte offset of the first frame in the stream. + */ + public FlacSeekTableSeekMap(FlacStreamMetadata flacStreamMetadata, long firstFrameOffset) { + this.flacStreamMetadata = flacStreamMetadata; + this.firstFrameOffset = firstFrameOffset; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return flacStreamMetadata.getDurationUs(); + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + Assertions.checkNotNull(flacStreamMetadata.seekTable); + long[] pointSampleNumbers = flacStreamMetadata.seekTable.pointSampleNumbers; + long[] pointOffsets = flacStreamMetadata.seekTable.pointOffsets; + + long targetSampleNumber = flacStreamMetadata.getSampleNumber(timeUs); + int index = + Util.binarySearchFloor( + pointSampleNumbers, + targetSampleNumber, + /* inclusive= */ true, + /* stayInBounds= */ false); + + long seekPointSampleNumber = index == -1 ? 0 : pointSampleNumbers[index]; + long seekPointOffsetFromFirstFrame = index == -1 ? 0 : pointOffsets[index]; + SeekPoint seekPoint = getSeekPoint(seekPointSampleNumber, seekPointOffsetFromFirstFrame); + if (seekPoint.timeUs == timeUs || index == pointSampleNumbers.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint secondSeekPoint = + getSeekPoint(pointSampleNumbers[index + 1], pointOffsets[index + 1]); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + private SeekPoint getSeekPoint(long sampleNumber, long offsetFromFirstFrame) { + long seekTimeUs = sampleNumber * C.MICROS_PER_SECOND / flacStreamMetadata.sampleRate; + long seekPosition = firstFrameOffset + offsetFromFirstFrame; + return new SeekPoint(seekTimeUs, seekPosition); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java index 255799c02..60386dcc3 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/Id3Peeker.java @@ -53,7 +53,7 @@ public final class Id3Peeker { Metadata metadata = null; while (true) { try { - input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH); + input.peekFully(scratch.data, /* offset= */ 0, Id3Decoder.ID3_HEADER_LENGTH); } catch (EOFException e) { // If input has less than ID3_HEADER_LENGTH, ignore the rest. break; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java index e454bd51c..b3155233d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/MpegAudioHeader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; @@ -55,12 +56,17 @@ public final class MpegAudioHeader { 160000 }; + private static final int SAMPLES_PER_FRAME_L1 = 384; + private static final int SAMPLES_PER_FRAME_L2 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V1 = 1152; + private static final int SAMPLES_PER_FRAME_L3_V2 = 576; + /** * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it * is invalid. */ public static int getFrameSize(int header) { - if ((header & 0xFFE00000) != 0xFFE00000) { + if (!isMagicPresent(header)) { return C.LENGTH_UNSET; } @@ -119,6 +125,36 @@ public final class MpegAudioHeader { } } + /** + * Returns the number of samples per frame associated with {@code header}, or {@link + * C#LENGTH_UNSET} if it is invalid. + */ + public static int getFrameSampleCount(int header) { + + if (!isMagicPresent(header)) { + return C.LENGTH_UNSET; + } + + int version = (header >>> 19) & 3; + if (version == 1) { + return C.LENGTH_UNSET; + } + + int layer = (header >>> 17) & 3; + if (layer == 0) { + return C.LENGTH_UNSET; + } + + // Those header values are not used but are checked for consistency with the other methods + int bitrateIndex = (header >>> 12) & 15; + int samplingRateIndex = (header >>> 10) & 3; + if (bitrateIndex == 0 || bitrateIndex == 0xF || samplingRateIndex == 3) { + return C.LENGTH_UNSET; + } + + return getFrameSizeInSamples(version, layer); + } + /** * Parses {@code headerData}, populating {@code header} with the parsed data. * @@ -128,7 +164,7 @@ public final class MpegAudioHeader { * is not a valid MPEG audio header. */ public static boolean populateHeader(int headerData, MpegAudioHeader header) { - if ((headerData & 0xFFE00000) != 0xFFE00000) { + if (!isMagicPresent(headerData)) { return false; } @@ -165,23 +201,20 @@ public final class MpegAudioHeader { int padding = (headerData >>> 9) & 1; int bitrate; int frameSize; - int samplesPerFrame; + int samplesPerFrame = getFrameSizeInSamples(version, layer); if (layer == 3) { // Layer I (layer == 3) bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; frameSize = (12 * 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 = 144 * bitrate / sampleRate + padding; } else { // Version 2 or 2.5. bitrate = BITRATE_V2[bitrateIndex - 1]; - samplesPerFrame = layer == 1 ? 576 : 1152; frameSize = (layer == 1 ? 72 : 144) * bitrate / sampleRate + padding; } } @@ -192,10 +225,26 @@ public final class MpegAudioHeader { return true; } + private static boolean isMagicPresent(int header) { + return (header & 0xFFE00000) == 0xFFE00000; + } + + private static int getFrameSizeInSamples(int version, int layer) { + switch (layer) { + case 1: + return version == 3 ? SAMPLES_PER_FRAME_L3_V1 : SAMPLES_PER_FRAME_L3_V2; // Layer III + case 2: + return SAMPLES_PER_FRAME_L2; // Layer II + case 3: + return SAMPLES_PER_FRAME_L1; // Layer I + } + throw new IllegalArgumentException(); + } + /** MPEG audio header version. */ public int version; /** The mime type. */ - public String mimeType; + @Nullable public String mimeType; /** Size of the frame associated with this header, in bytes. */ public int frameSize; /** Sample rate in samples per second. */ @@ -223,5 +272,4 @@ public final class MpegAudioHeader { this.bitrate = bitrate; this.samplesPerFrame = samplesPerFrame; } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/VorbisBitArray.java similarity index 96% rename from TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java rename to TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/VorbisBitArray.java index 958a2ef95..b498be4a3 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/VorbisBitArray.java @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor.ogg; +package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.util.Assertions; /** - * Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream. + * 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 class VorbisBitArray { private final byte[] data; private final int byteLimit; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java similarity index 80% rename from TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java rename to TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java index 1d7b50cd3..5066c3a7b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/VorbisUtil.java @@ -13,17 +13,87 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor.ogg; +package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Arrays; -/** - * Utility methods for parsing vorbis streams. - */ -/* package */ final class VorbisUtil { +/** Utility methods for parsing Vorbis streams. */ +public final class VorbisUtil { + + /** Vorbis comment header. */ + 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; + } + } + + /** Vorbis identification header. */ + 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; + } + } + + /** Vorbis setup header modes. */ + 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; + } + } private static final String TAG = "VorbisUtil"; @@ -45,7 +115,7 @@ import java.util.Arrays; } /** - * Reads a vorbis identification header from {@code headerData}. + * Reads a Vorbis identification header from {@code headerData}. * * @see Vorbis * spec/Identification header @@ -70,7 +140,7 @@ import java.util.Arrays; 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 + // 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, @@ -78,18 +148,41 @@ import java.util.Arrays; } /** - * Reads a vorbis comment header. + * 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. + * @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 If an error occurs parsing the comment header. */ public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) throws ParserException { + return readVorbisCommentHeader( + headerData, /* hasMetadataHeader= */ true, /* hasFramingBit= */ true); + } - verifyVorbisHeaderCapturePattern(0x03, headerData, false); + /** + * Reads a Vorbis comment header. + * + *

The data provided may not contain the Vorbis metadata common header and the framing bit. + * + * @see Vorbis + * spec/Comment header + * @param headerData A {@link ParsableByteArray} wrapping the header data. + * @param hasMetadataHeader Whether the {@code headerData} contains a Vorbis metadata common + * header preceding the comment header. + * @param hasFramingBit Whether the {@code headerData} contains a framing bit. + * @return A {@link VorbisUtil.CommentHeader} with all the comments. + * @throws ParserException If an error occurs parsing the comment header. + */ + public static CommentHeader readVorbisCommentHeader( + ParsableByteArray headerData, boolean hasMetadataHeader, boolean hasFramingBit) + throws ParserException { + + if (hasMetadataHeader) { + verifyVorbisHeaderCapturePattern(/* headerType= */ 0x03, headerData, /* quiet= */ false); + } int length = 7; int len = (int) headerData.readLittleEndianUnsignedInt(); @@ -106,7 +199,7 @@ import java.util.Arrays; comments[i] = headerData.readString(len); length += comments[i].length(); } - if ((headerData.readUnsignedByte() & 0x01) == 0) { + if (hasFramingBit && (headerData.readUnsignedByte() & 0x01) == 0) { throw new ParserException("framing bit expected to be set"); } length += 1; @@ -114,8 +207,8 @@ import java.util.Arrays; } /** - * Verifies whether the next bytes in {@code header} are a vorbis header of the given - * {@code headerType}. + * 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. @@ -123,9 +216,8 @@ import java.util.Arrays; * @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 quiet) - throws ParserException { + public static boolean verifyVorbisHeaderCapturePattern( + int headerType, ParsableByteArray header, boolean quiet) throws ParserException { if (header.bytesLeft() < 7) { if (quiet) { return false; @@ -158,12 +250,12 @@ import java.util.Arrays; } /** - * 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. + * 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 + * @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. @@ -218,40 +310,38 @@ import java.util.Arrays; 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); + if (mappingType != 0) { + Log.e(TAG, "mapping type other than 0 not supported: " + mappingType); + continue; + } + 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 } } } @@ -411,7 +501,7 @@ import java.util.Arrays; // Prevent instantiation. } - public static final class CodeBook { + private static final class CodeBook { public final int dimensions; public final int entries; @@ -429,69 +519,4 @@ import java.util.Arrays; } } - - 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/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java index caf12948a..f6b64245f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/amr/AmrExtractor.java @@ -140,7 +140,7 @@ public final class AmrExtractor implements Extractor { private ExtractorOutput extractorOutput; private TrackOutput trackOutput; - private @Nullable SeekMap seekMap; + @Nullable private SeekMap seekMap; private boolean hasOutputFormat; public AmrExtractor() { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java new file mode 100644 index 000000000..82e1636ba --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacBinarySearchSeeker.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.flac; + +import com.google.android.exoplayer2.extractor.BinarySearchSeeker; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.FlacFrameReader; +import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import java.io.IOException; + +/** + * A {@link SeekMap} implementation for FLAC stream using binary search. + * + *

This seeker performs seeking by using binary search within the stream, until it finds the + * frame that contains the target sample. + */ +/* package */ final class FlacBinarySearchSeeker extends BinarySearchSeeker { + + /** + * Creates a {@link FlacBinarySearchSeeker}. + * + * @param flacStreamMetadata The stream metadata. + * @param frameStartMarker The frame start marker, consisting of the 2 bytes by which every frame + * in the stream must start. + * @param firstFramePosition The byte offset of the first frame in the stream. + * @param inputLength The length of the stream in bytes. + */ + public FlacBinarySearchSeeker( + FlacStreamMetadata flacStreamMetadata, + int frameStartMarker, + long firstFramePosition, + long inputLength) { + super( + /* seekTimestampConverter= */ flacStreamMetadata::getSampleNumber, + new FlacTimestampSeeker(flacStreamMetadata, frameStartMarker), + flacStreamMetadata.getDurationUs(), + /* floorTimePosition= */ 0, + /* ceilingTimePosition= */ flacStreamMetadata.totalSamples, + /* floorBytePosition= */ firstFramePosition, + /* ceilingBytePosition= */ inputLength, + /* approxBytesPerFrame= */ flacStreamMetadata.getApproxBytesPerFrame(), + /* minimumSearchRange= */ Math.max( + FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize)); + } + + private static final class FlacTimestampSeeker implements TimestampSeeker { + + private final FlacStreamMetadata flacStreamMetadata; + private final int frameStartMarker; + private final SampleNumberHolder sampleNumberHolder; + + private FlacTimestampSeeker(FlacStreamMetadata flacStreamMetadata, int frameStartMarker) { + this.flacStreamMetadata = flacStreamMetadata; + this.frameStartMarker = frameStartMarker; + sampleNumberHolder = new SampleNumberHolder(); + } + + @Override + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetSampleNumber) + throws IOException, InterruptedException { + long searchPosition = input.getPosition(); + + // Find left frame. + long leftFrameFirstSampleNumber = findNextFrame(input); + long leftFramePosition = input.getPeekPosition(); + + input.advancePeekPosition( + Math.max(FlacConstants.MIN_FRAME_HEADER_SIZE, flacStreamMetadata.minFrameSize)); + + // Find right frame. + long rightFrameFirstSampleNumber = findNextFrame(input); + long rightFramePosition = input.getPeekPosition(); + + if (leftFrameFirstSampleNumber <= targetSampleNumber + && rightFrameFirstSampleNumber > targetSampleNumber) { + return TimestampSearchResult.targetFoundResult(leftFramePosition); + } else if (rightFrameFirstSampleNumber <= targetSampleNumber) { + return TimestampSearchResult.underestimatedResult( + rightFrameFirstSampleNumber, rightFramePosition); + } else { + return TimestampSearchResult.overestimatedResult( + leftFrameFirstSampleNumber, searchPosition); + } + } + + /** + * Searches for the next frame in {@code input}. + * + *

The peek position is advanced to the start of the found frame, or at the end of the stream + * if no frame was found. + * + * @param input The input from which to search (starting from the peek position). + * @return The number of the first sample in the found frame, or the total number of samples in + * the stream if no frame was found. + * @throws IOException If peeking from the input fails. In this case, there is no guarantee on + * the peek position. + * @throws InterruptedException If interrupted while peeking from input. In this case, there is + * no guarantee on the peek position. + */ + private long findNextFrame(ExtractorInput input) throws IOException, InterruptedException { + while (input.getPeekPosition() < input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE + && !FlacFrameReader.checkFrameHeaderFromPeek( + input, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) { + input.advancePeekPosition(1); + } + + if (input.getPeekPosition() >= input.getLength() - FlacConstants.MIN_FRAME_HEADER_SIZE) { + input.advancePeekPosition((int) (input.getLength() - input.getPeekPosition())); + return flacStreamMetadata.totalSamples; + } + + return sampleNumberHolder.sampleNumber; + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java new file mode 100644 index 000000000..96ccfa4fe --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flac/FlacExtractor.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.flac; + +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.extractor.FlacFrameReader; +import com.google.android.exoplayer2.extractor.FlacFrameReader.SampleNumberHolder; +import com.google.android.exoplayer2.extractor.FlacMetadataReader; +import com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; +import com.google.android.exoplayer2.util.FlacStreamMetadata; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Extracts data from FLAC container format. + * + *

The format specification can be found at https://xiph.org/flac/format.html. + */ +public final class FlacExtractor implements Extractor { + + /** Factory for {@link FlacExtractor} instances. */ + public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()}; + + /** + * Flags controlling the behavior of the extractor. Possible flag value is {@link + * #FLAG_DISABLE_ID3_METADATA}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_DISABLE_ID3_METADATA}) + public @interface Flags {} + + /** + * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not + * required. + */ + public static final int FLAG_DISABLE_ID3_METADATA = 1; + + /** Parser state. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + STATE_READ_ID3_METADATA, + STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES, + STATE_READ_STREAM_MARKER, + STATE_READ_METADATA_BLOCKS, + STATE_GET_FRAME_START_MARKER, + STATE_READ_FRAMES + }) + private @interface State {} + + private static final int STATE_READ_ID3_METADATA = 0; + private static final int STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES = 1; + private static final int STATE_READ_STREAM_MARKER = 2; + private static final int STATE_READ_METADATA_BLOCKS = 3; + private static final int STATE_GET_FRAME_START_MARKER = 4; + private static final int STATE_READ_FRAMES = 5; + + /** Arbitrary buffer length of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ + private static final int BUFFER_LENGTH = 32 * 1024; + + /** Value of an unknown sample number. */ + private static final int SAMPLE_NUMBER_UNKNOWN = -1; + + private final byte[] streamMarkerAndInfoBlock; + private final ParsableByteArray buffer; + private final boolean id3MetadataDisabled; + + private final SampleNumberHolder sampleNumberHolder; + + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + + private @State int state; + @Nullable private Metadata id3Metadata; + @MonotonicNonNull private FlacStreamMetadata flacStreamMetadata; + private int minFrameSize; + private int frameStartMarker; + @MonotonicNonNull private FlacBinarySearchSeeker binarySearchSeeker; + private int currentFrameBytesWritten; + private long currentFrameFirstSampleNumber; + + /** Constructs an instance with {@code flags = 0}. */ + public FlacExtractor() { + this(/* flags= */ 0); + } + + /** + * Constructs an instance. + * + * @param flags Flags that control the extractor's behavior. Possible flags are described by + * {@link Flags}. + */ + public FlacExtractor(int flags) { + streamMarkerAndInfoBlock = + new byte[FlacConstants.STREAM_MARKER_SIZE + FlacConstants.STREAM_INFO_BLOCK_SIZE]; + buffer = new ParsableByteArray(new byte[BUFFER_LENGTH], /* limit= */ 0); + id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0; + sampleNumberHolder = new SampleNumberHolder(); + state = STATE_READ_ID3_METADATA; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.peekId3Metadata(input, /* parseData= */ false); + return FlacMetadataReader.checkAndPeekStreamMarker(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + trackOutput = output.track(/* id= */ 0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + } + + @Override + public @ReadResult int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + switch (state) { + case STATE_READ_ID3_METADATA: + readId3Metadata(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES: + getStreamMarkerAndInfoBlockBytes(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_STREAM_MARKER: + readStreamMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_METADATA_BLOCKS: + readMetadataBlocks(input); + return Extractor.RESULT_CONTINUE; + case STATE_GET_FRAME_START_MARKER: + getFrameStartMarker(input); + return Extractor.RESULT_CONTINUE; + case STATE_READ_FRAMES: + return readFrames(input, seekPosition); + default: + throw new IllegalStateException(); + } + } + + @Override + public void seek(long position, long timeUs) { + if (position == 0) { + state = STATE_READ_ID3_METADATA; + } else if (binarySearchSeeker != null) { + binarySearchSeeker.setSeekTargetUs(timeUs); + } + currentFrameFirstSampleNumber = timeUs == 0 ? 0 : SAMPLE_NUMBER_UNKNOWN; + currentFrameBytesWritten = 0; + buffer.reset(); + } + + @Override + public void release() { + // Do nothing. + } + + // Private methods. + + private void readId3Metadata(ExtractorInput input) throws IOException, InterruptedException { + id3Metadata = FlacMetadataReader.readId3Metadata(input, /* parseData= */ !id3MetadataDisabled); + state = STATE_GET_STREAM_MARKER_AND_INFO_BLOCK_BYTES; + } + + private void getStreamMarkerAndInfoBlockBytes(ExtractorInput input) + throws IOException, InterruptedException { + input.peekFully(streamMarkerAndInfoBlock, 0, streamMarkerAndInfoBlock.length); + input.resetPeekPosition(); + state = STATE_READ_STREAM_MARKER; + } + + private void readStreamMarker(ExtractorInput input) throws IOException, InterruptedException { + FlacMetadataReader.readStreamMarker(input); + state = STATE_READ_METADATA_BLOCKS; + } + + private void readMetadataBlocks(ExtractorInput input) throws IOException, InterruptedException { + boolean isLastMetadataBlock = false; + FlacMetadataReader.FlacStreamMetadataHolder metadataHolder = + new FlacMetadataReader.FlacStreamMetadataHolder(flacStreamMetadata); + while (!isLastMetadataBlock) { + isLastMetadataBlock = FlacMetadataReader.readMetadataBlock(input, metadataHolder); + // Save the current metadata in case an exception occurs. + flacStreamMetadata = castNonNull(metadataHolder.flacStreamMetadata); + } + + Assertions.checkNotNull(flacStreamMetadata); + minFrameSize = Math.max(flacStreamMetadata.minFrameSize, FlacConstants.MIN_FRAME_HEADER_SIZE); + castNonNull(trackOutput) + .format(flacStreamMetadata.getFormat(streamMarkerAndInfoBlock, id3Metadata)); + + state = STATE_GET_FRAME_START_MARKER; + } + + private void getFrameStartMarker(ExtractorInput input) throws IOException, InterruptedException { + frameStartMarker = FlacMetadataReader.getFrameStartMarker(input); + castNonNull(extractorOutput) + .seekMap( + getSeekMap( + /* firstFramePosition= */ input.getPosition(), + /* streamLength= */ input.getLength())); + + state = STATE_READ_FRAMES; + } + + private @ReadResult int readFrames(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + Assertions.checkNotNull(trackOutput); + Assertions.checkNotNull(flacStreamMetadata); + + // Handle pending binary search seek if necessary. + if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) { + return binarySearchSeeker.handlePendingSeek(input, seekPosition); + } + + // Set current frame first sample number if it became unknown after seeking. + if (currentFrameFirstSampleNumber == SAMPLE_NUMBER_UNKNOWN) { + currentFrameFirstSampleNumber = + FlacFrameReader.getFirstSampleNumber(input, flacStreamMetadata); + return Extractor.RESULT_CONTINUE; + } + + // Copy more bytes into the buffer. + int currentLimit = buffer.limit(); + boolean foundEndOfInput = false; + if (currentLimit < BUFFER_LENGTH) { + int bytesRead = + input.read( + buffer.data, /* offset= */ currentLimit, /* length= */ BUFFER_LENGTH - currentLimit); + foundEndOfInput = bytesRead == C.RESULT_END_OF_INPUT; + if (!foundEndOfInput) { + buffer.setLimit(currentLimit + bytesRead); + } else if (buffer.bytesLeft() == 0) { + outputSampleMetadata(); + return Extractor.RESULT_END_OF_INPUT; + } + } + + // Search for a frame. + int positionBeforeFindingAFrame = buffer.getPosition(); + + // Skip frame search on the bytes within the minimum frame size. + if (currentFrameBytesWritten < minFrameSize) { + buffer.skipBytes(Math.min(minFrameSize - currentFrameBytesWritten, buffer.bytesLeft())); + } + + long nextFrameFirstSampleNumber = findFrame(buffer, foundEndOfInput); + int numberOfFrameBytes = buffer.getPosition() - positionBeforeFindingAFrame; + buffer.setPosition(positionBeforeFindingAFrame); + trackOutput.sampleData(buffer, numberOfFrameBytes); + currentFrameBytesWritten += numberOfFrameBytes; + + // Frame found. + if (nextFrameFirstSampleNumber != SAMPLE_NUMBER_UNKNOWN) { + outputSampleMetadata(); + currentFrameBytesWritten = 0; + currentFrameFirstSampleNumber = nextFrameFirstSampleNumber; + } + + if (buffer.bytesLeft() < FlacConstants.MAX_FRAME_HEADER_SIZE) { + // The next frame header may not fit in the rest of the buffer, so put the trailing bytes at + // the start of the buffer, and reset the position and limit. + System.arraycopy( + buffer.data, buffer.getPosition(), buffer.data, /* destPos= */ 0, buffer.bytesLeft()); + buffer.reset(buffer.bytesLeft()); + } + + return Extractor.RESULT_CONTINUE; + } + + private SeekMap getSeekMap(long firstFramePosition, long streamLength) { + Assertions.checkNotNull(flacStreamMetadata); + if (flacStreamMetadata.seekTable != null) { + return new FlacSeekTableSeekMap(flacStreamMetadata, firstFramePosition); + } else if (streamLength != C.LENGTH_UNSET && flacStreamMetadata.totalSamples > 0) { + binarySearchSeeker = + new FlacBinarySearchSeeker( + flacStreamMetadata, frameStartMarker, firstFramePosition, streamLength); + return binarySearchSeeker.getSeekMap(); + } else { + return new SeekMap.Unseekable(flacStreamMetadata.getDurationUs()); + } + } + + /** + * Searches for the start of a frame in {@code data}. + * + *

    + *
  • If the search is successful, the position is set to the start of the found frame. + *
  • Otherwise, the position is set to the first unsearched byte. + *
+ * + * @param data The array to be searched. + * @param foundEndOfInput If the end of input was met when filling in the {@code data}. + * @return The number of the first sample in the frame found, or {@code SAMPLE_NUMBER_UNKNOWN} if + * the search was not successful. + */ + private long findFrame(ParsableByteArray data, boolean foundEndOfInput) { + Assertions.checkNotNull(flacStreamMetadata); + + int frameOffset = data.getPosition(); + while (frameOffset <= data.limit() - FlacConstants.MAX_FRAME_HEADER_SIZE) { + data.setPosition(frameOffset); + if (FlacFrameReader.checkAndReadFrameHeader( + data, flacStreamMetadata, frameStartMarker, sampleNumberHolder)) { + data.setPosition(frameOffset); + return sampleNumberHolder.sampleNumber; + } + frameOffset++; + } + + if (foundEndOfInput) { + // Verify whether there is a frame of size < MAX_FRAME_HEADER_SIZE at the end of the stream by + // checking at every position at a distance between MAX_FRAME_HEADER_SIZE and minFrameSize + // from the buffer limit if it corresponds to a valid frame header. + // At every offset, the different possibilities are: + // 1. The current offset indicates the start of a valid frame header. In this case, consider + // that a frame has been found and stop searching. + // 2. A frame starting at the current offset would be invalid. In this case, keep looking for + // a valid frame header. + // 3. The current offset could be the start of a valid frame header, but there is not enough + // bytes remaining to complete the header. As the end of the file has been reached, this + // means that the current offset does not correspond to a new frame and that the last bytes + // of the last frame happen to be a valid partial frame header. This case can occur in two + // ways: + // 3.1. An attempt to read past the buffer is made when reading the potential frame header. + // 3.2. Reading the potential frame header does not exceed the buffer size, but exceeds the + // buffer limit. + // Note that the third case is very unlikely. It never happens if the end of the input has not + // been reached as it is always made sure that the buffer has at least MAX_FRAME_HEADER_SIZE + // bytes available when reading a potential frame header. + while (frameOffset <= data.limit() - minFrameSize) { + data.setPosition(frameOffset); + boolean frameFound; + try { + frameFound = + FlacFrameReader.checkAndReadFrameHeader( + data, flacStreamMetadata, frameStartMarker, sampleNumberHolder); + } catch (IndexOutOfBoundsException e) { + // Case 3.1. + frameFound = false; + } + if (data.getPosition() > data.limit()) { + // TODO: Remove (and update above comments) once [Internal ref: b/147657250] is fixed. + // Case 3.2. + frameFound = false; + } + if (frameFound) { + // Case 1. + data.setPosition(frameOffset); + return sampleNumberHolder.sampleNumber; + } + frameOffset++; + } + // The end of the frame is the end of the file. + data.setPosition(data.limit()); + } else { + data.setPosition(frameOffset); + } + + return SAMPLE_NUMBER_UNKNOWN; + } + + private void outputSampleMetadata() { + long timeUs = + currentFrameFirstSampleNumber + * C.MICROS_PER_SECOND + / castNonNull(flacStreamMetadata).sampleRate; + castNonNull(trackOutput) + .sampleMetadata( + timeUs, + C.BUFFER_FLAG_KEY_FRAME, + currentFrameBytesWritten, + /* offset= */ 0, + /* encryptionData= */ null); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index b10f2bf80..4a904844e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -69,9 +69,20 @@ import java.util.Collections; } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW : MimeTypes.AUDIO_MLAW; - int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; - Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, - Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); + Format format = + Format.createAudioSampleFormat( + /* id= */ null, + /* sampleMimeType= */ type, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 1, + /* sampleRate= */ 8000, + /* pcmEncoding= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); output.format(format); hasOutputFormat = true; } else if (audioFormat != AUDIO_FORMAT_AAC) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java index de6ed2710..f6835558f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -64,7 +63,7 @@ public final class FlvExtractor implements Extractor { private static final int TAG_TYPE_SCRIPT_DATA = 18; // FLV container identifier. - private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); + private static final int FLV_TAG = 0x00464c56; private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index c785865f6..55d58d78d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -15,11 +15,11 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import android.util.Pair; +import android.util.SparseArray; import androidx.annotation.CallSuper; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.util.Pair; -import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -149,6 +149,10 @@ public class MatroskaExtractor implements Extractor { 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_BLOCK_ADDITIONS = 0x75A1; + private static final int ID_BLOCK_MORE = 0xA6; + private static final int ID_BLOCK_ADD_ID = 0xEE; + private static final int ID_BLOCK_ADDITIONAL = 0xA5; private static final int ID_REFERENCE_BLOCK = 0xFB; private static final int ID_TRACKS = 0x1654AE6B; private static final int ID_TRACK_ENTRY = 0xAE; @@ -157,6 +161,7 @@ public class MatroskaExtractor implements Extractor { private static final int ID_FLAG_DEFAULT = 0x88; private static final int ID_FLAG_FORCED = 0x55AA; private static final int ID_DEFAULT_DURATION = 0x23E383; + private static final int ID_MAX_BLOCK_ADDITION_ID = 0x55EE; private static final int ID_NAME = 0x536E; private static final int ID_CODEC_ID = 0x86; private static final int ID_CODEC_PRIVATE = 0x63A2; @@ -215,6 +220,12 @@ public class MatroskaExtractor implements Extractor { private static final int ID_LUMNINANCE_MAX = 0x55D9; private static final int ID_LUMNINANCE_MIN = 0x55DA; + /** + * BlockAddID value for ITU T.35 metadata in a VP9 track. See also + * https://www.webmproject.org/docs/container/. + */ + private static final int BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 = 4; + private static final int LACING_NONE = 0; private static final int LACING_XIPH = 1; private static final int LACING_FIXED_SIZE = 2; @@ -225,26 +236,25 @@ public class MatroskaExtractor implements Extractor { 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". + * A template for the prefix that must be added to each subrip sample. + * + *

The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. 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}; + 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 + }; /** * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}. */ private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19; - /** - * A special end timecode indicating that a subrip 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 value by which to divide a time in microseconds to convert it to the unit of the last value * in a subrip timecode (milliseconds). @@ -261,14 +271,21 @@ public class MatroskaExtractor implements Extractor { private static final byte[] SSA_DIALOGUE_FORMAT = Util.getUtf8Bytes("Format: Start, End, " + "ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); /** - * A template for the prefix that must be added to each SSA sample. The 10 byte end timecode - * starting at {@link #SSA_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: "Dialogue: 0:00:00:00,0:00:00:00,". + * A template for the prefix that must be added to each SSA sample. + * + *

The display time of each subtitle is passed as {@code timeUs} to {@link + * TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to + * {@code timeUs}. Hence the start timecode is always zero. 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: "Dialogue: 0:00:00:00,0:00:00:00,". */ - private static final byte[] SSA_PREFIX = new byte[] {68, 105, 97, 108, 111, 103, 117, 101, 58, 32, - 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44}; + private static final byte[] SSA_PREFIX = + new byte[] { + 68, 105, 97, 108, 111, 103, 117, 101, 58, 32, 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44, + 48, 58, 48, 48, 58, 48, 48, 58, 48, 48, 44 + }; /** * The byte offset of the end timecode in {@link #SSA_PREFIX}. */ @@ -278,14 +295,6 @@ public class MatroskaExtractor implements Extractor { * in an SSA timecode (1/100ths of a second). */ private static final long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000; - /** - * A special end timecode indicating that an SSA 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[] SSA_TIMECODE_EMPTY = - new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; /** * The format of an SSA timecode. */ @@ -323,6 +332,7 @@ public class MatroskaExtractor implements Extractor { private final ParsableByteArray subtitleSample; private final ParsableByteArray encryptionInitializationVector; private final ParsableByteArray encryptionSubsampleData; + private final ParsableByteArray blockAdditionalData; private ByteBuffer encryptionSubsampleDataBuffer; private long segmentContentSize; @@ -350,30 +360,33 @@ public class MatroskaExtractor implements Extractor { private LongArray cueClusterPositions; private boolean seenClusterPositionForCurrentCuePoint; + // Reading state. + private boolean haveOutputSample; + // Block reading state. private int blockState; private long blockTimeUs; private long blockDurationUs; - private int blockLacingSampleIndex; - private int blockLacingSampleCount; - private int[] blockLacingSampleSizes; + private int blockSampleIndex; + private int blockSampleCount; + private int[] blockSampleSizes; private int blockTrackNumber; private int blockTrackNumberLength; @C.BufferFlags private int blockFlags; + private int blockAdditionalId; + private boolean blockHasReferenceBlock; - // Sample reading state. + // Sample writing state. private int sampleBytesRead; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; private boolean sampleEncodingHandled; private boolean sampleSignalByteRead; - private boolean sampleInitializationVectorRead; private boolean samplePartitionCountRead; - private byte sampleSignalByte; private int samplePartitionCount; - private int sampleCurrentNalBytesRemaining; - private int sampleBytesWritten; - private boolean sampleRead; - private boolean sampleSeenReferenceBlock; + private byte sampleSignalByte; + private boolean sampleInitializationVectorRead; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -401,6 +414,7 @@ public class MatroskaExtractor implements Extractor { subtitleSample = new ParsableByteArray(); encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE); encryptionSubsampleData = new ParsableByteArray(); + blockAdditionalData = new ParsableByteArray(); } @Override @@ -420,7 +434,7 @@ public class MatroskaExtractor implements Extractor { blockState = BLOCK_STATE_START; reader.reset(); varintReader.reset(); - resetSample(); + resetWriteSampleData(); for (int i = 0; i < tracks.size(); i++) { tracks.valueAt(i).reset(); } @@ -434,9 +448,9 @@ public class MatroskaExtractor implements Extractor { @Override public final int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - sampleRead = false; + haveOutputSample = false; boolean continueReading = true; - while (continueReading && !sampleRead) { + while (continueReading && !haveOutputSample) { continueReading = reader.read(input); if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) { return Extractor.RESULT_SEEK; @@ -479,6 +493,8 @@ public class MatroskaExtractor implements Extractor { case ID_CUE_POINT: case ID_CUE_TRACK_POSITIONS: case ID_BLOCK_GROUP: + case ID_BLOCK_ADDITIONS: + case ID_BLOCK_MORE: case ID_PROJECTION: case ID_COLOUR: case ID_MASTERING_METADATA: @@ -499,6 +515,7 @@ public class MatroskaExtractor implements Extractor { case ID_FLAG_DEFAULT: case ID_FLAG_FORCED: case ID_DEFAULT_DURATION: + case ID_MAX_BLOCK_ADDITION_ID: case ID_CODEC_DELAY: case ID_SEEK_PRE_ROLL: case ID_CHANNELS: @@ -518,6 +535,7 @@ public class MatroskaExtractor implements Extractor { case ID_MAX_CLL: case ID_MAX_FALL: case ID_PROJECTION_TYPE: + case ID_BLOCK_ADD_ID: return EbmlProcessor.ELEMENT_TYPE_UNSIGNED_INT; case ID_DOC_TYPE: case ID_NAME: @@ -531,6 +549,7 @@ public class MatroskaExtractor implements Extractor { case ID_BLOCK: case ID_CODEC_PRIVATE: case ID_PROJECTION_PRIVATE: + case ID_BLOCK_ADDITIONAL: return EbmlProcessor.ELEMENT_TYPE_BINARY; case ID_DURATION: case ID_SAMPLING_FREQUENCY: @@ -606,7 +625,7 @@ public class MatroskaExtractor implements Extractor { } break; case ID_BLOCK_GROUP: - sampleSeenReferenceBlock = false; + blockHasReferenceBlock = false; break; case ID_CONTENT_ENCODING: // TODO: check and fail if more than one content encoding is present. @@ -663,11 +682,24 @@ public class MatroskaExtractor implements Extractor { // 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.BUFFER_FLAG_KEY_FRAME; + // Commit sample metadata. + int sampleOffset = 0; + for (int i = 0; i < blockSampleCount; i++) { + sampleOffset += blockSampleSizes[i]; + } + Track track = tracks.get(blockTrackNumber); + for (int i = 0; i < blockSampleCount; i++) { + long sampleTimeUs = blockTimeUs + (i * track.defaultSampleDurationNs) / 1000; + int sampleFlags = blockFlags; + if (i == 0 && !blockHasReferenceBlock) { + // If the ReferenceBlock element was not found in this block, then the first frame is a + // keyframe. + sampleFlags |= C.BUFFER_FLAG_KEY_FRAME; + } + int sampleSize = blockSampleSizes[i]; + sampleOffset -= sampleSize; // The offset is to the end of the sample. + commitSampleToOutput(track, sampleTimeUs, sampleFlags, sampleSize, sampleOffset); } - commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); blockState = BLOCK_STATE_START; break; case ID_CONTENT_ENCODING: @@ -760,6 +792,9 @@ public class MatroskaExtractor implements Extractor { case ID_DEFAULT_DURATION: currentTrack.defaultSampleDurationNs = (int) value; break; + case ID_MAX_BLOCK_ADDITION_ID: + currentTrack.maxBlockAdditionId = (int) value; + break; case ID_CODEC_DELAY: currentTrack.codecDelayNs = value; break; @@ -773,7 +808,7 @@ public class MatroskaExtractor implements Extractor { currentTrack.audioBitDepth = (int) value; break; case ID_REFERENCE_BLOCK: - sampleSeenReferenceBlock = true; + blockHasReferenceBlock = true; break; case ID_CONTENT_ENCODING_ORDER: // This extractor only supports one ContentEncoding element and hence the order has to be 0. @@ -914,6 +949,9 @@ public class MatroskaExtractor implements Extractor { break; } break; + case ID_BLOCK_ADD_ID: + blockAdditionalId = (int) value; + break; default: break; } @@ -1068,43 +1106,38 @@ public class MatroskaExtractor implements Extractor { 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; + blockSampleCount = 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, 1); + blockSampleSizes[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); + blockSampleCount = (scratch.data[3] & 0xFF) + 1; + blockSampleSizes = ensureArrayCapacity(blockSampleSizes, blockSampleCount); if (lacing == LACING_FIXED_SIZE) { int blockLacingSampleSize = - (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount; - Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize); + (contentSize - blockTrackNumberLength - 4) / blockSampleCount; + Arrays.fill(blockSampleSizes, 0, blockSampleCount, blockLacingSampleSize); } else if (lacing == LACING_XIPH) { int totalSamplesSize = 0; int headerSize = 4; - for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { - blockLacingSampleSizes[sampleIndex] = 0; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; int byteValue; do { readScratch(input, ++headerSize); byteValue = scratch.data[headerSize - 1] & 0xFF; - blockLacingSampleSizes[sampleIndex] += byteValue; + blockSampleSizes[sampleIndex] += byteValue; } while (byteValue == 0xFF); - totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 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; + for (int sampleIndex = 0; sampleIndex < blockSampleCount - 1; sampleIndex++) { + blockSampleSizes[sampleIndex] = 0; readScratch(input, ++headerSize); if (scratch.data[headerSize - 1] == 0) { throw new ParserException("No valid varint length mask found"); @@ -1132,11 +1165,13 @@ public class MatroskaExtractor implements Extractor { 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]; + blockSampleSizes[sampleIndex] = + sampleIndex == 0 + ? intReadValue + : blockSampleSizes[sampleIndex - 1] + intReadValue; + totalSamplesSize += blockSampleSizes[sampleIndex]; } - blockLacingSampleSizes[blockLacingSampleCount - 1] = + blockSampleSizes[blockSampleCount - 1] = contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; } else { // Lacing is always in the range 0--3. @@ -1152,67 +1187,93 @@ public class MatroskaExtractor implements Extractor { blockFlags = (isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0) | (isInvisible ? C.BUFFER_FLAG_DECODE_ONLY : 0); blockState = BLOCK_STATE_DATA; - blockLacingSampleIndex = 0; + blockSampleIndex = 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 = blockTimeUs - + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000; - commitSampleToOutput(track, sampleTimeUs); - blockLacingSampleIndex++; + // For SimpleBlock, we can write sample data and immediately commit the corresponding + // sample metadata. + while (blockSampleIndex < blockSampleCount) { + int sampleSize = writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + long sampleTimeUs = + blockTimeUs + (blockSampleIndex * track.defaultSampleDurationNs) / 1000; + commitSampleToOutput(track, sampleTimeUs, blockFlags, sampleSize, /* offset= */ 0); + blockSampleIndex++; } 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]); + // For Block, we need to wait until the end of the BlockGroup element before committing + // sample metadata. This is so that we can handle ReferenceBlock (which can be used to + // infer whether the first sample in the block is a keyframe), and BlockAdditions (which + // can contain additional sample data to append) contained in the block group. Just output + // the sample data, storing the final sample sizes for when we commit the metadata. + while (blockSampleIndex < blockSampleCount) { + blockSampleSizes[blockSampleIndex] = + writeSampleData(input, track, blockSampleSizes[blockSampleIndex]); + blockSampleIndex++; + } } break; + case ID_BLOCK_ADDITIONAL: + if (blockState != BLOCK_STATE_DATA) { + return; + } + handleBlockAdditionalData( + tracks.get(blockTrackNumber), blockAdditionalId, input, contentSize); + break; default: throw new ParserException("Unexpected id: " + id); } } - private void commitSampleToOutput(Track track, long timeUs) { - if (track.trueHdSampleRechunker != null) { - track.trueHdSampleRechunker.sampleMetadata(track, timeUs); + protected void handleBlockAdditionalData( + Track track, int blockAdditionalId, ExtractorInput input, int contentSize) + throws IOException, InterruptedException { + if (blockAdditionalId == BLOCK_ADDITIONAL_ID_VP9_ITU_T_35 + && CODEC_ID_VP9.equals(track.codecId)) { + blockAdditionalData.reset(contentSize); + input.readFully(blockAdditionalData.data, 0, contentSize); } else { - if (CODEC_ID_SUBRIP.equals(track.codecId)) { - commitSubtitleSample( - track, - SUBRIP_TIMECODE_FORMAT, - SUBRIP_PREFIX_END_TIMECODE_OFFSET, - SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, - SUBRIP_TIMECODE_EMPTY); - } else if (CODEC_ID_ASS.equals(track.codecId)) { - commitSubtitleSample( - track, - SSA_TIMECODE_FORMAT, - SSA_PREFIX_END_TIMECODE_OFFSET, - SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, - SSA_TIMECODE_EMPTY); - } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); + // Unhandled block additional data. + input.skipFully(contentSize); } - sampleRead = true; - resetSample(); } - private void resetSample() { - sampleBytesRead = 0; - sampleBytesWritten = 0; - sampleCurrentNalBytesRemaining = 0; - sampleEncodingHandled = false; - sampleSignalByteRead = false; - samplePartitionCountRead = false; - samplePartitionCount = 0; - sampleSignalByte = (byte) 0; - sampleInitializationVectorRead = false; - sampleStrippedBytes.reset(); + private void commitSampleToOutput( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (track.trueHdSampleRechunker != null) { + track.trueHdSampleRechunker.sampleMetadata(track, timeUs, flags, size, offset); + } else { + if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) { + if (blockSampleCount > 1) { + Log.w(TAG, "Skipping subtitle sample in laced block."); + } else if (blockDurationUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping subtitle sample with no duration."); + } else { + setSubtitleEndTime(track.codecId, blockDurationUs, subtitleSample.data); + // Note: If we ever want to support DRM protected subtitles then we'll need to output the + // appropriate encryption data here. + track.output.sampleData(subtitleSample, subtitleSample.limit()); + size += subtitleSample.limit(); + } + } + + if ((flags & C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA) != 0) { + if (blockSampleCount > 1) { + // There were multiple samples in the block. Appending the additional data to the last + // sample doesn't make sense. Skip instead. + flags &= ~C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + } else { + // Append supplemental data. + int blockAdditionalSize = blockAdditionalData.limit(); + track.output.sampleData(blockAdditionalData, blockAdditionalSize); + size += blockAdditionalSize; + } + } + track.output.sampleMetadata(timeUs, flags, size, offset, track.cryptoData); + } + haveOutputSample = true; } /** @@ -1232,14 +1293,24 @@ public class MatroskaExtractor implements Extractor { scratch.setLimit(requiredLength); } - private void writeSampleData(ExtractorInput input, Track track, int size) + /** + * Writes data for a single sample to the track output. + * + * @param input The input from which to read sample data. + * @param track The track to output the sample to. + * @param size The size of the sample data on the input side. + * @return The final size of the written sample. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int writeSampleData(ExtractorInput input, Track track, int size) throws IOException, InterruptedException { if (CODEC_ID_SUBRIP.equals(track.codecId)) { writeSubtitleSampleData(input, SUBRIP_PREFIX, size); - return; + return finishWriteSampleData(); } else if (CODEC_ID_ASS.equals(track.codecId)) { writeSubtitleSampleData(input, SSA_PREFIX, size); - return; + return finishWriteSampleData(); } TrackOutput output = track.output; @@ -1328,6 +1399,21 @@ public class MatroskaExtractor implements Extractor { // If the sample has header stripping, prepare to read/output the stripped bytes first. sampleStrippedBytes.reset(track.sampleStrippedBytes, track.sampleStrippedBytes.length); } + + if (track.maxBlockAdditionId > 0) { + blockFlags |= C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA; + blockAdditionalData.reset(); + // If there is supplemental data, the structure of the sample data is: + // sample size (4 bytes) || sample data || supplemental data + scratch.reset(/* limit= */ 4); + scratch.data[0] = (byte) ((size >> 24) & 0xFF); + scratch.data[1] = (byte) ((size >> 16) & 0xFF); + scratch.data[2] = (byte) ((size >> 8) & 0xFF); + scratch.data[3] = (byte) (size & 0xFF); + output.sampleData(scratch, 4); + sampleBytesWritten += 4; + } + sampleEncodingHandled = true; } size += sampleStrippedBytes.limit(); @@ -1349,8 +1435,9 @@ public class MatroskaExtractor implements Extractor { 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); + writeToTarget( + input, nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); // Write a start code for the current NAL unit. @@ -1359,17 +1446,21 @@ public class MatroskaExtractor implements Extractor { sampleBytesWritten += 4; } else { // Write the payload of the NAL unit. - sampleCurrentNalBytesRemaining -= - readToOutput(input, output, sampleCurrentNalBytesRemaining); + int bytesWritten = writeToOutput(input, output, sampleCurrentNalBytesRemaining); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; + sampleCurrentNalBytesRemaining -= bytesWritten; } } } else { if (track.trueHdSampleRechunker != null) { Assertions.checkState(sampleStrippedBytes.limit() == 0); - track.trueHdSampleRechunker.startSample(input, blockFlags, size); + track.trueHdSampleRechunker.startSample(input); } while (sampleBytesRead < size) { - readToOutput(input, output, size - sampleBytesRead); + int bytesWritten = writeToOutput(input, output, size - sampleBytesRead); + sampleBytesRead += bytesWritten; + sampleBytesWritten += bytesWritten; } } @@ -1384,6 +1475,32 @@ public class MatroskaExtractor implements Extractor { output.sampleData(vorbisNumPageSamples, 4); sampleBytesWritten += 4; } + + return finishWriteSampleData(); + } + + /** + * Called by {@link #writeSampleData(ExtractorInput, Track, int)} when the sample has been + * written. Returns the final sample size and resets state for the next sample. + */ + private int finishWriteSampleData() { + int sampleSize = sampleBytesWritten; + resetWriteSampleData(); + return sampleSize; + } + + /** Resets state used by {@link #writeSampleData(ExtractorInput, Track, int)}. */ + private void resetWriteSampleData() { + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + sampleEncodingHandled = false; + sampleSignalByteRead = false; + samplePartitionCountRead = false; + samplePartitionCount = 0; + sampleSignalByte = (byte) 0; + sampleInitializationVectorRead = false; + sampleStrippedBytes.reset(); } private void writeSubtitleSampleData(ExtractorInput input, byte[] samplePrefix, int size) @@ -1402,67 +1519,89 @@ public class MatroskaExtractor implements Extractor { // the correct end timecode, which we might not have yet. } - private void commitSubtitleSample(Track track, String timecodeFormat, int endTimecodeOffset, - long lastTimecodeValueScalingFactor, byte[] emptyTimecode) { - setSampleDuration(subtitleSample.data, blockDurationUs, timecodeFormat, endTimecodeOffset, - lastTimecodeValueScalingFactor, emptyTimecode); - // Note: If we ever want to support DRM protected subtitles then we'll need to output the - // appropriate encryption data here. - track.output.sampleData(subtitleSample, subtitleSample.limit()); - sampleBytesWritten += subtitleSample.limit(); + /** + * Overwrites the end timecode in {@code subtitleData} with the correctly formatted time derived + * from {@code durationUs}. + * + *

See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use + * the duration as the end timecode. + * + * @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP} or {@link #CODEC_ID_ASS}. + * @param durationUs The duration of the sample, in microseconds. + * @param subtitleData The subtitle sample in which to overwrite the end timecode (output + * parameter). + */ + private static void setSubtitleEndTime(String codecId, long durationUs, byte[] subtitleData) { + byte[] endTimecode; + int endTimecodeOffset; + switch (codecId) { + case CODEC_ID_SUBRIP: + endTimecode = + formatSubtitleTimecode( + durationUs, SUBRIP_TIMECODE_FORMAT, SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = SUBRIP_PREFIX_END_TIMECODE_OFFSET; + break; + case CODEC_ID_ASS: + endTimecode = + formatSubtitleTimecode( + durationUs, SSA_TIMECODE_FORMAT, SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR); + endTimecodeOffset = SSA_PREFIX_END_TIMECODE_OFFSET; + break; + default: + throw new IllegalArgumentException(); + } + System.arraycopy(endTimecode, 0, subtitleData, endTimecodeOffset, endTimecode.length); } - private static void setSampleDuration(byte[] subripSampleData, long durationUs, - String timecodeFormat, int endTimecodeOffset, long lastTimecodeValueScalingFactor, - byte[] emptyTimecode) { + /** + * Formats {@code timeUs} using {@code timecodeFormat}, and sets it as the end timecode in {@code + * subtitleSampleData}. + */ + private static byte[] formatSubtitleTimecode( + long timeUs, String timecodeFormat, long lastTimecodeValueScalingFactor) { + Assertions.checkArgument(timeUs != C.TIME_UNSET); byte[] timeCodeData; - if (durationUs == C.TIME_UNSET) { - timeCodeData = emptyTimecode; - } else { - int hours = (int) (durationUs / (3600 * C.MICROS_PER_SECOND)); - durationUs -= (hours * 3600 * C.MICROS_PER_SECOND); - int minutes = (int) (durationUs / (60 * C.MICROS_PER_SECOND)); - durationUs -= (minutes * 60 * C.MICROS_PER_SECOND); - int seconds = (int) (durationUs / C.MICROS_PER_SECOND); - durationUs -= (seconds * C.MICROS_PER_SECOND); - int lastValue = (int) (durationUs / lastTimecodeValueScalingFactor); - timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, timecodeFormat, hours, minutes, - seconds, lastValue)); - } - System.arraycopy(timeCodeData, 0, subripSampleData, endTimecodeOffset, emptyTimecode.length); + int hours = (int) (timeUs / (3600 * C.MICROS_PER_SECOND)); + timeUs -= (hours * 3600 * C.MICROS_PER_SECOND); + int minutes = (int) (timeUs / (60 * C.MICROS_PER_SECOND)); + timeUs -= (minutes * 60 * C.MICROS_PER_SECOND); + int seconds = (int) (timeUs / C.MICROS_PER_SECOND); + timeUs -= (seconds * C.MICROS_PER_SECOND); + int lastValue = (int) (timeUs / lastTimecodeValueScalingFactor); + timeCodeData = + Util.getUtf8Bytes( + String.format(Locale.US, timecodeFormat, hours, minutes, seconds, lastValue)); + return timeCodeData; } /** * 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) + private void writeToTarget(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) + private int writeToOutput(ExtractorInput input, TrackOutput output, int length) throws IOException, InterruptedException { - int bytesRead; + int bytesWritten; int strippedBytesLeft = sampleStrippedBytes.bytesLeft(); if (strippedBytesLeft > 0) { - bytesRead = Math.min(length, strippedBytesLeft); - output.sampleData(sampleStrippedBytes, bytesRead); + bytesWritten = Math.min(length, strippedBytesLeft); + output.sampleData(sampleStrippedBytes, bytesWritten); } else { - bytesRead = output.sampleData(input, length, false); + bytesWritten = output.sampleData(input, length, false); } - sampleBytesRead += bytesRead; - sampleBytesWritten += bytesRead; - return bytesRead; + return bytesWritten; } /** @@ -1496,6 +1635,16 @@ public class MatroskaExtractor implements Extractor { sizes[cuePointsSize - 1] = (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]); durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1]; + + long lastDurationUs = durationsUs[cuePointsSize - 1]; + if (lastDurationUs <= 0) { + Log.w(TAG, "Discarding last cue point with unexpected duration: " + lastDurationUs); + sizes = Arrays.copyOf(sizes, sizes.length - 1); + offsets = Arrays.copyOf(offsets, offsets.length - 1); + durationsUs = Arrays.copyOf(durationsUs, durationsUs.length - 1); + timesUs = Arrays.copyOf(timesUs, timesUs.length - 1); + } + cueTimesUs = null; cueClusterPositions = null; return new ChunkIndex(sizes, offsets, durationsUs, timesUs); @@ -1637,10 +1786,11 @@ public class MatroskaExtractor implements Extractor { private final byte[] syncframePrefix; private boolean foundSyncframe; - private int sampleCount; + private int chunkSampleCount; + private long chunkTimeUs; + private @C.BufferFlags int chunkFlags; private int chunkSize; - private long timeUs; - private @C.BufferFlags int blockFlags; + private int chunkOffset; public TrueHdSampleRechunker() { syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; @@ -1648,47 +1798,44 @@ public class MatroskaExtractor implements Extractor { public void reset() { foundSyncframe = false; + chunkSampleCount = 0; } - public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size) - throws IOException, InterruptedException { - if (!foundSyncframe) { - input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); - input.resetPeekPosition(); - if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { - return; - } - foundSyncframe = true; - sampleCount = 0; + public void startSample(ExtractorInput input) throws IOException, InterruptedException { + if (foundSyncframe) { + return; } - if (sampleCount == 0) { - // This is the first sample in the chunk, so reset the block flags and chunk size. - this.blockFlags = blockFlags; + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if (Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == 0) { + return; + } + foundSyncframe = true; + } + + public void sampleMetadata( + Track track, long timeUs, @C.BufferFlags int flags, int size, int offset) { + if (!foundSyncframe) { + return; + } + if (chunkSampleCount++ == 0) { + // This is the first sample in the chunk. + chunkTimeUs = timeUs; + chunkFlags = flags; chunkSize = 0; } chunkSize += size; - } - - public void sampleMetadata(Track track, long timeUs) { - if (!foundSyncframe) { - return; + chunkOffset = offset; // The offset is to the end of the sample. + if (chunkSampleCount >= Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + outputPendingSampleMetadata(track); } - if (sampleCount++ == 0) { - // This is the first sample in the chunk, so update the timestamp. - this.timeUs = timeUs; - } - if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { - // We haven't read enough samples to output a chunk. - return; - } - track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); - sampleCount = 0; } public void outputPendingSampleMetadata(Track track) { - if (foundSyncframe && sampleCount > 0) { - track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); - sampleCount = 0; + if (chunkSampleCount > 0) { + track.output.sampleMetadata( + chunkTimeUs, chunkFlags, chunkSize, chunkOffset, track.cryptoData); + chunkSampleCount = 0; } } } @@ -1713,6 +1860,7 @@ public class MatroskaExtractor implements Extractor { public int number; public int type; public int defaultSampleDurationNs; + public int maxBlockAdditionId; public boolean hasContentEncryption; public byte[] sampleStrippedBytes; public TrackOutput.CryptoData cryptoData; @@ -1828,9 +1976,9 @@ public class MatroskaExtractor implements Extractor { initializationData = new ArrayList<>(3); initializationData.add(codecPrivate); initializationData.add( - ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(codecDelayNs).array()); + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(codecDelayNs).array()); initializationData.add( - ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(seekPreRollNs).array()); + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(seekPreRollNs).array()); break; case CODEC_ID_AAC: mimeType = MimeTypes.AUDIO_AAC; @@ -2032,6 +2180,7 @@ public class MatroskaExtractor implements Extractor { } /** Returns the HDR Static Info as defined in CTA-861.3. */ + @Nullable private byte[] getHdrStaticInfo() { // Are all fields present. if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE @@ -2044,7 +2193,7 @@ public class MatroskaExtractor implements Extractor { } byte[] hdrStaticInfoData = new byte[25]; - ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData); + ByteBuffer hdrStaticInfo = ByteBuffer.wrap(hdrStaticInfoData).order(ByteOrder.LITTLE_ENDIAN); hdrStaticInfo.put((byte) 0); // Type. hdrStaticInfo.putShort((short) ((primaryRChromaticityX * MAX_CHROMATICITY) + 0.5f)); hdrStaticInfo.putShort((short) ((primaryRChromaticityY * MAX_CHROMATICITY) + 0.5f)); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 6134f042c..7a25677c5 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.MlltFrame; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; @@ -95,9 +94,9 @@ public final class Mp3Extractor implements Extractor { */ private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00; - private static final int SEEK_HEADER_XING = Util.getIntegerCodeForString("Xing"); - private static final int SEEK_HEADER_INFO = Util.getIntegerCodeForString("Info"); - private static final int SEEK_HEADER_VBRI = Util.getIntegerCodeForString("VBRI"); + private static final int SEEK_HEADER_XING = 0x58696e67; + private static final int SEEK_HEADER_INFO = 0x496e666f; + private static final int SEEK_HEADER_VBRI = 0x56425249; private static final int SEEK_HEADER_UNSET = 0; @Flags private final int flags; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java index 7094f327c..c51b68a7c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java @@ -60,7 +60,7 @@ import com.google.android.exoplayer2.util.Util; return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); } - long dataSize = frame.readUnsignedIntToInt(); + long dataSize = frame.readUnsignedInt(); long[] tableOfContents = new long[100]; for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); @@ -88,7 +88,7 @@ import com.google.android.exoplayer2.util.Util; * Entries are in the range [0, 255], but are stored as long integers for convenience. Null if the * table of contents was missing from the header, in which case seeking is not be supported. */ - private final @Nullable long[] tableOfContents; + @Nullable private final long[] tableOfContents; private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { this( diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index f66c1f5d2..e86a873ed 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.mp4; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -51,334 +50,337 @@ import java.util.List; public static final int EXTENDS_TO_END_SIZE = 0; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp"); + public static final int TYPE_ftyp = 0x66747970; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1"); + public static final int TYPE_avc1 = 0x61766331; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3"); + public static final int TYPE_avc3 = 0x61766333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC"); + public static final int TYPE_avcC = 0x61766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1"); + public static final int TYPE_hvc1 = 0x68766331; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1"); + public static final int TYPE_hev1 = 0x68657631; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC"); + public static final int TYPE_hvcC = 0x68766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08"); + public static final int TYPE_vp08 = 0x76703038; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); + public static final int TYPE_vp09 = 0x76703039; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); + public static final int TYPE_vpcC = 0x76706343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_av01 = Util.getIntegerCodeForString("av01"); + public static final int TYPE_av01 = 0x61763031; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_av1C = Util.getIntegerCodeForString("av1C"); + public static final int TYPE_av1C = 0x61763143; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvav = Util.getIntegerCodeForString("dvav"); + public static final int TYPE_dvav = 0x64766176; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dva1 = Util.getIntegerCodeForString("dva1"); + public static final int TYPE_dva1 = 0x64766131; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvhe = Util.getIntegerCodeForString("dvhe"); + public static final int TYPE_dvhe = 0x64766865; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvh1 = Util.getIntegerCodeForString("dvh1"); + public static final int TYPE_dvh1 = 0x64766831; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvcC = Util.getIntegerCodeForString("dvcC"); + public static final int TYPE_dvcC = 0x64766343; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dvvC = Util.getIntegerCodeForString("dvvC"); + public static final int TYPE_dvvC = 0x64767643; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_s263 = Util.getIntegerCodeForString("s263"); + public static final int TYPE_s263 = 0x73323633; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_d263 = Util.getIntegerCodeForString("d263"); + public static final int TYPE_d263 = 0x64323633; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat"); + public static final int TYPE_mdat = 0x6d646174; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a"); + public static final int TYPE_mp4a = 0x6d703461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE__mp3 = Util.getIntegerCodeForString(".mp3"); + public static final int TYPE__mp3 = 0x2e6d7033; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_wave = Util.getIntegerCodeForString("wave"); + public static final int TYPE_wave = 0x77617665; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_lpcm = Util.getIntegerCodeForString("lpcm"); + public static final int TYPE_lpcm = 0x6c70636d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sowt = Util.getIntegerCodeForString("sowt"); + public static final int TYPE_sowt = 0x736f7774; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3"); + public static final int TYPE_ac_3 = 0x61632d33; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3"); + public static final int TYPE_dac3 = 0x64616333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3"); + public static final int TYPE_ec_3 = 0x65632d33; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3"); + public static final int TYPE_dec3 = 0x64656333; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ac_4 = Util.getIntegerCodeForString("ac-4"); + public static final int TYPE_ac_4 = 0x61632d34; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dac4 = Util.getIntegerCodeForString("dac4"); + public static final int TYPE_dac4 = 0x64616334; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsc = Util.getIntegerCodeForString("dtsc"); + public static final int TYPE_dtsc = 0x64747363; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsh = Util.getIntegerCodeForString("dtsh"); + public static final int TYPE_dtsh = 0x64747368; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtsl = Util.getIntegerCodeForString("dtsl"); + public static final int TYPE_dtsl = 0x6474736c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dtse = Util.getIntegerCodeForString("dtse"); + public static final int TYPE_dtse = 0x64747365; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ddts = Util.getIntegerCodeForString("ddts"); + public static final int TYPE_ddts = 0x64647473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt"); + public static final int TYPE_tfdt = 0x74666474; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd"); + public static final int TYPE_tfhd = 0x74666864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trex = Util.getIntegerCodeForString("trex"); + public static final int TYPE_trex = 0x74726578; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trun = Util.getIntegerCodeForString("trun"); + public static final int TYPE_trun = 0x7472756e; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx"); + public static final int TYPE_sidx = 0x73696478; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_moov = Util.getIntegerCodeForString("moov"); + public static final int TYPE_moov = 0x6d6f6f76; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd"); + public static final int TYPE_mvhd = 0x6d766864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_trak = Util.getIntegerCodeForString("trak"); + public static final int TYPE_trak = 0x7472616b; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia"); + public static final int TYPE_mdia = 0x6d646961; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_minf = Util.getIntegerCodeForString("minf"); + public static final int TYPE_minf = 0x6d696e66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl"); + public static final int TYPE_stbl = 0x7374626c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_esds = Util.getIntegerCodeForString("esds"); + public static final int TYPE_esds = 0x65736473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_moof = Util.getIntegerCodeForString("moof"); + public static final int TYPE_moof = 0x6d6f6f66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_traf = Util.getIntegerCodeForString("traf"); + public static final int TYPE_traf = 0x74726166; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex"); + public static final int TYPE_mvex = 0x6d766578; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mehd = Util.getIntegerCodeForString("mehd"); + public static final int TYPE_mehd = 0x6d656864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd"); + public static final int TYPE_tkhd = 0x746b6864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_edts = Util.getIntegerCodeForString("edts"); + public static final int TYPE_edts = 0x65647473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_elst = Util.getIntegerCodeForString("elst"); + public static final int TYPE_elst = 0x656c7374; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd"); + public static final int TYPE_mdhd = 0x6d646864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr"); + public static final int TYPE_hdlr = 0x68646c72; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd"); + public static final int TYPE_stsd = 0x73747364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh"); + public static final int TYPE_pssh = 0x70737368; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf"); + public static final int TYPE_sinf = 0x73696e66; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_schm = Util.getIntegerCodeForString("schm"); + public static final int TYPE_schm = 0x7363686d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_schi = Util.getIntegerCodeForString("schi"); + public static final int TYPE_schi = 0x73636869; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc"); + public static final int TYPE_tenc = 0x74656e63; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_encv = Util.getIntegerCodeForString("encv"); + public static final int TYPE_encv = 0x656e6376; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_enca = Util.getIntegerCodeForString("enca"); + public static final int TYPE_enca = 0x656e6361; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_frma = Util.getIntegerCodeForString("frma"); + public static final int TYPE_frma = 0x66726d61; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz"); + public static final int TYPE_saiz = 0x7361697a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_saio = Util.getIntegerCodeForString("saio"); + public static final int TYPE_saio = 0x7361696f; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sbgp = Util.getIntegerCodeForString("sbgp"); + public static final int TYPE_sbgp = 0x73626770; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sgpd = Util.getIntegerCodeForString("sgpd"); + public static final int TYPE_sgpd = 0x73677064; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid"); + public static final int TYPE_uuid = 0x75756964; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_senc = Util.getIntegerCodeForString("senc"); + public static final int TYPE_senc = 0x73656e63; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp"); + public static final int TYPE_pasp = 0x70617370; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML"); + public static final int TYPE_TTML = 0x54544d4c; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd"); + public static final int TYPE_vmhd = 0x766d6864; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v"); + public static final int TYPE_mp4v = 0x6d703476; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stts = Util.getIntegerCodeForString("stts"); + public static final int TYPE_stts = 0x73747473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stss = Util.getIntegerCodeForString("stss"); + public static final int TYPE_stss = 0x73747373; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts"); + public static final int TYPE_ctts = 0x63747473; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc"); + public static final int TYPE_stsc = 0x73747363; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz"); + public static final int TYPE_stsz = 0x7374737a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stz2 = Util.getIntegerCodeForString("stz2"); + public static final int TYPE_stz2 = 0x73747a32; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stco = Util.getIntegerCodeForString("stco"); + public static final int TYPE_stco = 0x7374636f; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_co64 = Util.getIntegerCodeForString("co64"); + public static final int TYPE_co64 = 0x636f3634; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g"); + public static final int TYPE_tx3g = 0x74783367; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt"); + public static final int TYPE_wvtt = 0x77767474; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp"); + public static final int TYPE_stpp = 0x73747070; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_c608 = Util.getIntegerCodeForString("c608"); + public static final int TYPE_c608 = 0x63363038; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_samr = Util.getIntegerCodeForString("samr"); + public static final int TYPE_samr = 0x73616d72; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); + public static final int TYPE_sawb = 0x73617762; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_udta = Util.getIntegerCodeForString("udta"); + public static final int TYPE_udta = 0x75647461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + public static final int TYPE_meta = 0x6d657461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_keys = Util.getIntegerCodeForString("keys"); + public static final int TYPE_keys = 0x6b657973; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst"); + public static final int TYPE_ilst = 0x696c7374; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); + public static final int TYPE_mean = 0x6d65616e; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_name = Util.getIntegerCodeForString("name"); + public static final int TYPE_name = 0x6e616d65; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_data = Util.getIntegerCodeForString("data"); + public static final int TYPE_data = 0x64617461; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg"); + public static final int TYPE_emsg = 0x656d7367; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d"); + public static final int TYPE_st3d = 0x73743364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d"); + public static final int TYPE_sv3d = 0x73763364; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_proj = Util.getIntegerCodeForString("proj"); + public static final int TYPE_proj = 0x70726f6a; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); + public static final int TYPE_camm = 0x63616d6d; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_alac = Util.getIntegerCodeForString("alac"); + public static final int TYPE_alac = 0x616c6163; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_alaw = Util.getIntegerCodeForString("alaw"); + public static final int TYPE_alaw = 0x616c6177; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_ulaw = Util.getIntegerCodeForString("ulaw"); + public static final int TYPE_ulaw = 0x756c6177; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_Opus = Util.getIntegerCodeForString("Opus"); + public static final int TYPE_Opus = 0x4f707573; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dOps = Util.getIntegerCodeForString("dOps"); + public static final int TYPE_dOps = 0x644f7073; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_fLaC = Util.getIntegerCodeForString("fLaC"); + public static final int TYPE_fLaC = 0x664c6143; @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_dfLa = Util.getIntegerCodeForString("dfLa"); + public static final int TYPE_dfLa = 0x64664c61; + + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_twos = 0x74776f73; public final int type; @@ -459,7 +461,8 @@ import java.util.List; * @param type The leaf type. * @return The child leaf of the given type, or null if no such child exists. */ - public @Nullable LeafAtom getLeafAtomOfType(int type) { + @Nullable + public LeafAtom getLeafAtomOfType(int type) { int childrenSize = leafChildren.size(); for (int i = 0; i < childrenSize; i++) { LeafAtom atom = leafChildren.get(i); @@ -479,7 +482,8 @@ import java.util.List; * @param type The container type. * @return The child container of the given type, or null if no such child exists. */ - public @Nullable ContainerAtom getContainerAtomOfType(int type) { + @Nullable + public ContainerAtom getContainerAtomOfType(int type) { int childrenSize = containerChildren.size(); for (int i = 0; i < childrenSize; i++) { ContainerAtom atom = containerChildren.get(i); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index c4e6ef17c..17c541ce0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.extractor.mp4; import static com.google.android.exoplayer2.util.MimeTypes.getMimeTypeFromMp4ObjectType; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -42,19 +42,34 @@ import java.util.Collections; import java.util.List; /** Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. */ -@SuppressWarnings({"ConstantField", "ConstantCaseForConstants"}) +@SuppressWarnings({"ConstantField"}) /* package */ final class AtomParsers { private static final String TAG = "AtomParsers"; - private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); - private static final int TYPE_soun = Util.getIntegerCodeForString("soun"); - private static final int TYPE_text = Util.getIntegerCodeForString("text"); - private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); - private static final int TYPE_subt = Util.getIntegerCodeForString("subt"); - private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp"); - private static final int TYPE_meta = Util.getIntegerCodeForString("meta"); - private static final int TYPE_mdta = Util.getIntegerCodeForString("mdta"); + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_vide = 0x76696465; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_soun = 0x736f756e; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_text = 0x74657874; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_sbtl = 0x7362746c; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_subt = 0x73756274; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_clcp = 0x636c6370; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_meta = 0x6d657461; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_mdta = 0x6d647461; /** * The threshold number of samples to trim from the start/end of an audio track when applying an @@ -348,9 +363,7 @@ import java.util.List; } long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale); - if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) { - // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. - // This implementation does not support applying both gapless metadata and an edit list. + if (track.editListDurations == null) { Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); return new TrackSampleTable( track, offsets, sizes, maximumSize, timestamps, flags, durationUs); @@ -420,10 +433,15 @@ import java.util.List; long editDuration = Util.scaleLargeTimestamp( track.editListDurations[i], track.timescale, track.movieTimescale); - startIndices[i] = Util.binarySearchCeil(timestamps, editMediaTime, true, true); + startIndices[i] = + Util.binarySearchFloor( + timestamps, editMediaTime, /* inclusive= */ true, /* stayInBounds= */ true); endIndices[i] = Util.binarySearchCeil( - timestamps, editMediaTime + editDuration, omitClippedSample, false); + timestamps, + editMediaTime + editDuration, + /* inclusive= */ omitClippedSample, + /* stayInBounds= */ false); while (startIndices[i] < endIndices[i] && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) { // Applying the edit correctly would require prerolling from the previous sync sample. In @@ -461,7 +479,7 @@ import java.util.List; long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long timeInSegmentUs = Util.scaleLargeTimestamp( - timestamps[j] - editMediaTime, C.MICROS_PER_SECOND, track.timescale); + Math.max(0, timestamps[j] - editMediaTime), C.MICROS_PER_SECOND, track.timescale); editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { editedMaximumSize = sizes[j]; @@ -766,6 +784,7 @@ import java.util.List; || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt + || childAtomType == Atom.TYPE_twos || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alaw @@ -897,8 +916,7 @@ import java.util.List; out.nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; } else if (childAtomType == Atom.TYPE_dvcC || childAtomType == Atom.TYPE_dvvC) { DolbyVisionConfig dolbyVisionConfig = DolbyVisionConfig.parse(parent); - // TODO: Support profiles 4, 8 and 9 once we have a way to fall back to AVC/HEVC decoding. - if (dolbyVisionConfig != null && dolbyVisionConfig.profile == 5) { + if (dolbyVisionConfig != null) { codecs = dolbyVisionConfig.codecs; mimeType = MimeTypes.VIDEO_DOLBY_VISION; } @@ -1027,6 +1045,7 @@ import java.util.List; int channelCount; int sampleRate; + @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); @@ -1087,6 +1106,10 @@ import java.util.List; mimeType = MimeTypes.AUDIO_AMR_WB; } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT; + } else if (atomType == Atom.TYPE_twos) { + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; } else if (atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; } else if (atomType == Atom.TYPE_alac) { @@ -1116,8 +1139,8 @@ import java.util.List; 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. + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See [Internal: b/10903778]. Pair audioSpecificConfig = CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData); sampleRate = audioSpecificConfig.first; @@ -1162,14 +1185,17 @@ import java.util.List; initializationData = new byte[childAtomBodySize]; parent.setPosition(childPosition + Atom.FULL_HEADER_SIZE); parent.readBytes(initializationData, /* offset= */ 0, childAtomBodySize); + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data, + // which is more reliable. See https://github.com/google/ExoPlayer/pull/6629. + Pair audioSpecificConfig = + CodecSpecificDataUtil.parseAlacAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; } childPosition += childAtomSize; } if (out.format == null && mimeType != null) { - // TODO: Determine the correct PCM encoding. - @C.PcmEncoding int pcmEncoding = - MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE; out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding, initializationData == null ? null : Collections.singletonList(initializationData), diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 3bf460468..c0d1581c3 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -15,10 +15,10 @@ */ package com.google.android.exoplayer2.extractor.mp4; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; import android.util.Pair; import android.util.SparseArray; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -57,6 +57,7 @@ import java.util.List; import java.util.UUID; /** Extracts data from the FMP4 container format. */ +@SuppressWarnings("ConstantField") public class FragmentedMp4Extractor implements Extractor { /** Factory for {@link FragmentedMp4Extractor} instances. */ @@ -106,8 +107,8 @@ public class FragmentedMp4Extractor implements Extractor { private static final String TAG = "FragmentedMp4Extractor"; - @SuppressWarnings("ConstantField") - private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); + @SuppressWarnings("ConstantCaseForConstants") + private static final int SAMPLE_GROUP_TYPE_seig = 0x73656967; 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}; @@ -123,11 +124,10 @@ public class FragmentedMp4Extractor implements Extractor { // Workarounds. @Flags private final int flags; - private final @Nullable Track sideloadedTrack; + @Nullable private final Track sideloadedTrack; // Sideloaded data. private final List closedCaptionFormats; - private final @Nullable DrmInitData sideloadedDrmInitData; // Track-linked data bundle, accessible as a whole through trackID. private final SparseArray trackBundles; @@ -140,7 +140,7 @@ public class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray scratch; // Adjusts sample timestamps. - private final @Nullable TimestampAdjuster timestampAdjuster; + @Nullable private final TimestampAdjuster timestampAdjuster; private final EventMessageEncoder eventMessageEncoder; @@ -148,7 +148,7 @@ public class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray atomHeader; private final ArrayDeque containerAtoms; private final ArrayDeque pendingMetadataSampleInfos; - private final @Nullable TrackOutput additionalEmsgTrackOutput; + @Nullable private final TrackOutput additionalEmsgTrackOutput; private int parserState; private int atomType; @@ -166,7 +166,6 @@ public class FragmentedMp4Extractor implements Extractor { private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; private boolean processSeiNalUnitPayload; - private boolean isAc4HeaderRequired; // Extractor output. private ExtractorOutput extractorOutput; @@ -184,7 +183,7 @@ public class FragmentedMp4Extractor implements Extractor { * @param flags Flags that control the extractor's behavior. */ public FragmentedMp4Extractor(@Flags int flags) { - this(flags, null); + this(flags, /* timestampAdjuster= */ null); } /** @@ -192,7 +191,7 @@ public class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, @Nullable TimestampAdjuster timestampAdjuster) { - this(flags, timestampAdjuster, null, null); + this(flags, timestampAdjuster, /* sideloadedTrack= */ null, Collections.emptyList()); } /** @@ -200,15 +199,12 @@ public class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * receive a moov box in the input data. Null if a moov box is expected. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the - * pssh boxes (if present) will be used. */ public FragmentedMp4Extractor( @Flags int flags, @Nullable TimestampAdjuster timestampAdjuster, - @Nullable Track sideloadedTrack, - @Nullable DrmInitData sideloadedDrmInitData) { - this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, Collections.emptyList()); + @Nullable Track sideloadedTrack) { + this(flags, timestampAdjuster, sideloadedTrack, Collections.emptyList()); } /** @@ -216,8 +212,6 @@ public class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * receive a moov box in the input data. Null if a moov box is expected. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the - * pssh boxes (if present) will be used. * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed * caption channels to expose. */ @@ -225,10 +219,13 @@ public class FragmentedMp4Extractor implements Extractor { @Flags int flags, @Nullable TimestampAdjuster timestampAdjuster, @Nullable Track sideloadedTrack, - @Nullable DrmInitData sideloadedDrmInitData, List closedCaptionFormats) { - this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, - closedCaptionFormats, null); + this( + flags, + timestampAdjuster, + sideloadedTrack, + closedCaptionFormats, + /* additionalEmsgTrackOutput= */ null); } /** @@ -236,8 +233,6 @@ public class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor will not * receive a moov box in the input data. Null if a moov box is expected. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the - * pssh boxes (if present) will be used. * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed * caption channels to expose. * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages @@ -248,13 +243,11 @@ public class FragmentedMp4Extractor implements Extractor { @Flags int flags, @Nullable TimestampAdjuster timestampAdjuster, @Nullable Track sideloadedTrack, - @Nullable DrmInitData sideloadedDrmInitData, List closedCaptionFormats, @Nullable TrackOutput additionalEmsgTrackOutput) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; - this.sideloadedDrmInitData = sideloadedDrmInitData; this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; eventMessageEncoder = new EventMessageEncoder(); @@ -300,7 +293,6 @@ public class FragmentedMp4Extractor implements Extractor { pendingMetadataSampleBytes = 0; pendingSeekTimeUs = timeUs; containerAtoms.clear(); - isAc4HeaderRequired = false; enterReadingAtomHeaderState(); } @@ -470,8 +462,7 @@ public class FragmentedMp4Extractor implements Extractor { private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); - DrmInitData drmInitData = sideloadedDrmInitData != null ? sideloadedDrmInitData - : getDrmInitDataFromAtoms(moov.leafChildren); + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); // Read declaration of track fragments in the Moov box. ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); @@ -549,9 +540,8 @@ public class FragmentedMp4Extractor implements Extractor { private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { parseMoof(moof, trackBundles, flags, scratchBytes); - // If drm init data is sideloaded, we ignore pssh boxes. - DrmInitData drmInitData = sideloadedDrmInitData != null ? null - : getDrmInitDataFromAtoms(moof.leafChildren); + + @Nullable DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); if (drmInitData != null) { int trackCount = trackBundles.size(); for (int i = 0; i < trackCount; i++) { @@ -674,9 +664,9 @@ public class FragmentedMp4Extractor implements Extractor { 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 defaultSampleDescriptionIndex = trex.readInt() - 1; + int defaultSampleDuration = trex.readInt(); + int defaultSampleSize = trex.readInt(); int defaultSampleFlags = trex.readInt(); return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex, @@ -761,8 +751,9 @@ public class FragmentedMp4Extractor implements Extractor { } } - private static void parseTruns(ContainerAtom traf, TrackBundle trackBundle, long decodeTime, - @Flags int flags) { + private static void parseTruns( + ContainerAtom traf, TrackBundle trackBundle, long decodeTime, @Flags int flags) + throws ParserException { int trunCount = 0; int totalSampleCount = 0; List leafChildren = traf.leafChildren; @@ -881,13 +872,20 @@ public class FragmentedMp4Extractor implements Extractor { 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; + ? tfhd.readInt() - 1 + : defaultSampleValues.sampleDescriptionIndex; + int defaultSampleDuration = + ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.duration; + int defaultSampleSize = + ((atomFlags & 0x10 /* default_sample_size_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.size; + int defaultSampleFlags = + ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) + ? tfhd.readInt() + : defaultSampleValues.flags; trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration, defaultSampleSize, defaultSampleFlags); return trackBundle; @@ -920,16 +918,22 @@ public class FragmentedMp4Extractor implements Extractor { /** * 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 trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into which + * parsed data should be placed. * @param index Index of the track run in the fragment. * @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 decode. * @return The starting position of samples for the next run. */ - private static int parseTrun(TrackBundle trackBundle, int index, long decodeTime, - @Flags int flags, ParsableByteArray trun, int trackRunStart) { + private static int parseTrun( + TrackBundle trackBundle, + int index, + long decodeTime, + @Flags int flags, + ParsableByteArray trun, + int trackRunStart) + throws ParserException { trun.setPosition(Atom.HEADER_SIZE); int fullAtom = trun.readInt(); int atomFlags = Atom.parseFullAtomFlags(fullAtom); @@ -947,7 +951,7 @@ public class FragmentedMp4Extractor implements Extractor { boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0; int firstSampleFlags = defaultSampleValues.flags; if (firstSampleFlagsPresent) { - firstSampleFlags = trun.readUnsignedIntToInt(); + firstSampleFlags = trun.readInt(); } boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0; @@ -958,20 +962,20 @@ public class FragmentedMp4Extractor implements Extractor { // 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; + long edtsOffsetUs = 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 = + edtsOffsetUs = Util.scaleLargeTimestamp( - track.editListMediaTimes[0], C.MILLIS_PER_SECOND, track.timescale); + track.editListMediaTimes[0], C.MICROS_PER_SECOND, track.timescale); } int[] sampleSizeTable = fragment.sampleSizeTable; - int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable; - long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable; + int[] sampleCompositionTimeOffsetUsTable = fragment.sampleCompositionTimeOffsetUsTable; + long[] sampleDecodingTimeUsTable = fragment.sampleDecodingTimeUsTable; boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable; boolean workaroundEveryVideoFrameIsSyncFrame = track.type == C.TRACK_TYPE_VIDEO @@ -982,9 +986,10 @@ public class FragmentedMp4Extractor implements Extractor { long cumulativeTime = index > 0 ? fragment.nextFragmentDecodeTime : decodeTime; for (int i = trackRunStart; i < trackRunEnd; 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 sampleDuration = + checkNonNegative(sampleDurationsPresent ? trun.readInt() : defaultSampleValues.duration); + int sampleSize = + checkNonNegative(sampleSizesPresent ? trun.readInt() : defaultSampleValues.size); int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; if (sampleCompositionTimeOffsetsPresent) { @@ -994,13 +999,13 @@ public class FragmentedMp4Extractor implements Extractor { // 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 * C.MILLIS_PER_SECOND) / timescale); + sampleCompositionTimeOffsetUsTable[i] = + (int) ((sampleOffset * C.MICROS_PER_SECOND) / timescale); } else { - sampleCompositionTimeOffsetTable[i] = 0; + sampleCompositionTimeOffsetUsTable[i] = 0; } - sampleDecodingTimeTable[i] = - Util.scaleLargeTimestamp(cumulativeTime, C.MILLIS_PER_SECOND, timescale) - edtsOffset; + sampleDecodingTimeUsTable[i] = + Util.scaleLargeTimestamp(cumulativeTime, C.MICROS_PER_SECOND, timescale) - edtsOffsetUs; sampleSizeTable[i] = sampleSize; sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); @@ -1010,6 +1015,13 @@ public class FragmentedMp4Extractor implements Extractor { return trackRunEnd; } + private static int checkNonNegative(int value) throws ParserException { + if (value < 0) { + throw new ParserException("Unexpected negtive value: " + value); + } + return value; + } + private static void parseUuid(ParsableByteArray uuid, TrackFragment out, byte[] extendedTypeScratch) throws ParserException { uuid.setPosition(Atom.HEADER_SIZE); @@ -1258,19 +1270,28 @@ public class FragmentedMp4Extractor implements Extractor { sampleSize -= Atom.HEADER_SIZE; input.skipFully(Atom.HEADER_SIZE); } - sampleBytesWritten = currentTrackBundle.outputSampleEncryptionData(); + + if (MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType)) { + // AC4 samples need to be prefixed with a clear sample header. + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, Ac4Util.SAMPLE_HEADER_SIZE); + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + currentTrackBundle.output.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } else { + sampleBytesWritten = + currentTrackBundle.outputSampleEncryptionData(sampleSize, /* clearHeaderSize= */ 0); + } sampleSize += sampleBytesWritten; parserState = STATE_READING_SAMPLE_CONTINUE; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(currentTrackBundle.track.format.sampleMimeType); } TrackFragment fragment = currentTrackBundle.fragment; Track track = currentTrackBundle.track; TrackOutput output = currentTrackBundle.output; int sampleIndex = currentTrackBundle.currentSampleIndex; - long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + long sampleTimeUs = fragment.getSamplePresentationTimeUs(sampleIndex); if (timestampAdjuster != null) { sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs); } @@ -1328,14 +1349,6 @@ public class FragmentedMp4Extractor implements Extractor { } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - output.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; - } while (sampleBytesWritten < sampleSize) { int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); sampleBytesWritten += writtenBytes; @@ -1408,6 +1421,7 @@ public class FragmentedMp4Extractor implements Extractor { } /** Returns DrmInitData from leaf atoms. */ + @Nullable private static DrmInitData getDrmInitDataFromAtoms(List leafChildren) { ArrayList schemeDatas = null; int leafChildrenSize = leafChildren.size(); @@ -1467,8 +1481,11 @@ public class FragmentedMp4Extractor implements Extractor { */ private static final class TrackBundle { + private static final int SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH = 8; + public final TrackOutput output; public final TrackFragment fragment; + public final ParsableByteArray scratch; public Track track; public DefaultSampleValues defaultSampleValues; @@ -1483,6 +1500,7 @@ public class FragmentedMp4Extractor implements Extractor { public TrackBundle(TrackOutput output) { this.output = output; fragment = new TrackFragment(); + scratch = new ParsableByteArray(); encryptionSignalByte = new ParsableByteArray(1); defaultInitializationVector = new ParsableByteArray(); } @@ -1517,10 +1535,9 @@ public class FragmentedMp4Extractor implements Extractor { * @param timeUs The seek time, in microseconds. */ public void seek(long timeUs) { - long timeMs = C.usToMs(timeUs); int searchIndex = currentSampleIndex; while (searchIndex < fragment.sampleCount - && fragment.getSamplePresentationTime(searchIndex) < timeMs) { + && fragment.getSamplePresentationTimeUs(searchIndex) < timeUs) { if (fragment.sampleIsSyncFrameTable[searchIndex]) { firstSampleToOutputIndex = searchIndex; } @@ -1550,9 +1567,13 @@ public class FragmentedMp4Extractor implements Extractor { /** * Outputs the encryption data for the current sample. * + * @param sampleSize The size of the current sample in bytes, excluding any additional clear + * header that will be prefixed to the sample by the extractor. + * @param clearHeaderSize The size of a clear header that will be prefixed to the sample by the + * extractor, or 0. * @return The number of written bytes. */ - public int outputSampleEncryptionData() { + public int outputSampleEncryptionData(int sampleSize, int clearHeaderSize) { TrackEncryptionBox encryptionBox = getEncryptionBoxIfEncrypted(); if (encryptionBox == null) { return 0; @@ -1571,23 +1592,61 @@ public class FragmentedMp4Extractor implements Extractor { vectorSize = initVectorData.length; } - boolean subsampleEncryption = fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean haveSubsampleEncryptionTable = + fragment.sampleHasSubsampleEncryptionTable(currentSampleIndex); + boolean writeSubsampleEncryptionData = haveSubsampleEncryptionTable || clearHeaderSize != 0; // Write the signal byte, containing the vector size and the subsample encryption flag. - encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); + encryptionSignalByte.data[0] = + (byte) (vectorSize | (writeSubsampleEncryptionData ? 0x80 : 0)); encryptionSignalByte.setPosition(0); output.sampleData(encryptionSignalByte, 1); // Write the vector. output.sampleData(initializationVectorData, vectorSize); - // If we don't have subsample encryption data, we're done. - if (!subsampleEncryption) { + + if (!writeSubsampleEncryptionData) { return 1 + vectorSize; } - // Write the subsample encryption data. + + if (!haveSubsampleEncryptionTable) { + // The sample is fully encrypted, except for the additional clear header that the extractor + // is going to prefix. We need to synthesize subsample encryption data that takes the header + // into account. + scratch.reset(SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + // subsampleCount = 1 (unsigned short) + scratch.data[0] = (byte) 0; + scratch.data[1] = (byte) 1; + // clearDataSize = clearHeaderSize (unsigned short) + scratch.data[2] = (byte) ((clearHeaderSize >> 8) & 0xFF); + scratch.data[3] = (byte) (clearHeaderSize & 0xFF); + // encryptedDataSize = sampleSize (unsigned short) + scratch.data[4] = (byte) ((sampleSize >> 24) & 0xFF); + scratch.data[5] = (byte) ((sampleSize >> 16) & 0xFF); + scratch.data[6] = (byte) ((sampleSize >> 8) & 0xFF); + scratch.data[7] = (byte) (sampleSize & 0xFF); + output.sampleData(scratch, SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH); + return 1 + vectorSize + SINGLE_SUBSAMPLE_ENCRYPTION_DATA_LENGTH; + } + ParsableByteArray subsampleEncryptionData = fragment.sampleEncryptionData; int subsampleCount = subsampleEncryptionData.readUnsignedShort(); subsampleEncryptionData.skipBytes(-2); int subsampleDataLength = 2 + 6 * subsampleCount; + + if (clearHeaderSize != 0) { + // We need to account for the additional clear header by adding clearHeaderSize to + // clearDataSize for the first subsample specified in the subsample encryption data. + scratch.reset(subsampleDataLength); + scratch.readBytes(subsampleEncryptionData.data, /* offset= */ 0, subsampleDataLength); + subsampleEncryptionData.skipBytes(subsampleDataLength); + + int clearDataSize = (scratch.data[2] & 0xFF) << 8 | (scratch.data[3] & 0xFF); + int adjustedClearDataSize = clearDataSize + clearHeaderSize; + scratch.data[2] = (byte) ((adjustedClearDataSize >> 8) & 0xFF); + scratch.data[3] = (byte) (adjustedClearDataSize & 0xFF); + subsampleEncryptionData = scratch; + } + output.sampleData(subsampleEncryptionData, subsampleDataLength); return 1 + vectorSize + subsampleDataLength; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index e9c9f7faf..4f65836b7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; @@ -27,8 +28,6 @@ import com.google.android.exoplayer2.metadata.id3.InternalFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; -import java.nio.ByteBuffer; /** Utilities for handling metadata in MP4. */ /* package */ final class MetadataUtil { @@ -36,72 +35,245 @@ import java.nio.ByteBuffer; private static final String TAG = "MetadataUtil"; // Codes that start with the copyright character (omitted) and have equivalent ID3 frames. - private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam"); - private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk"); - private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt"); - private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day"); - private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART"); - private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too"); - private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb"); - private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com"); - private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt"); - private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr"); - private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen"); + private static final int SHORT_TYPE_NAME_1 = 0x006e616d; + private static final int SHORT_TYPE_NAME_2 = 0x0074726b; + private static final int SHORT_TYPE_COMMENT = 0x00636d74; + private static final int SHORT_TYPE_YEAR = 0x00646179; + private static final int SHORT_TYPE_ARTIST = 0x00415254; + private static final int SHORT_TYPE_ENCODER = 0x00746f6f; + private static final int SHORT_TYPE_ALBUM = 0x00616c62; + private static final int SHORT_TYPE_COMPOSER_1 = 0x00636f6d; + private static final int SHORT_TYPE_COMPOSER_2 = 0x00777274; + private static final int SHORT_TYPE_LYRICS = 0x006c7972; + private static final int SHORT_TYPE_GENRE = 0x0067656e; // Codes that have equivalent ID3 frames. - private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr"); - private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre"); - private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp"); - private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk"); - private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn"); - private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo"); - private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil"); - private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART"); - private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm"); - private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal"); - private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar"); - private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa"); - private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco"); + private static final int TYPE_COVER_ART = 0x636f7672; + private static final int TYPE_GENRE = 0x676e7265; + private static final int TYPE_GROUPING = 0x00677270; + private static final int TYPE_DISK_NUMBER = 0x6469736b; + private static final int TYPE_TRACK_NUMBER = 0x74726b6e; + private static final int TYPE_TEMPO = 0x746d706f; + private static final int TYPE_COMPILATION = 0x6370696c; + private static final int TYPE_ALBUM_ARTIST = 0x61415254; + private static final int TYPE_SORT_TRACK_NAME = 0x736f6e6d; + private static final int TYPE_SORT_ALBUM = 0x736f616c; + private static final int TYPE_SORT_ARTIST = 0x736f6172; + private static final int TYPE_SORT_ALBUM_ARTIST = 0x736f6161; + private static final int TYPE_SORT_COMPOSER = 0x736f636f; // Types that do not have equivalent ID3 frames. - private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng"); - private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap"); - private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn"); - private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh"); + private static final int TYPE_RATING = 0x72746e67; + private static final int TYPE_GAPLESS_ALBUM = 0x70676170; + private static final int TYPE_TV_SORT_SHOW = 0x736f736e; + private static final int TYPE_TV_SHOW = 0x74767368; // Type for items that are intended for internal use by the player. - private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----"); + private static final int TYPE_INTERNAL = 0x2d2d2d2d; private static final int PICTURE_TYPE_FRONT_COVER = 3; // Standard genres. - private static final String[] STANDARD_GENRES = new String[] { - // These are the official ID3v1 genres. - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", - "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", - "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", - "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", - "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul", - "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", - "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", - "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", - "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", - "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", - "Hard Rock", - // These were made up by the authors of Winamp and later added to the ID3 spec. - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", - "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", - "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", - "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", - "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", - "Euro-House", "Dance Hall", - // These were med up by the authors of Winamp but have not been added to the ID3 spec. - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk", - "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", - "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", - "Jpop", "Synthpop" - }; + @VisibleForTesting + /* package */ static final String[] STANDARD_GENRES = + new String[] { + // These are the official ID3v1 genres. + "Blues", + "Classic Rock", + "Country", + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", + "Metal", + "New Age", + "Oldies", + "Other", + "Pop", + "R&B", + "Rap", + "Reggae", + "Rock", + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "AlternRock", + "Bass", + "Soul", + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", + "Psychadelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", + // Genres made up by the authors of Winamp (v1.91) and later added to the ID3 spec. + "Folk", + "Folk-Rock", + "National Folk", + "Swing", + "Fast Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A capella", + "Euro-House", + "Dance Hall", + // Genres made up by the authors of Winamp (v1.91) but have not been added to the ID3 spec. + "Goa", + "Drum & Bass", + "Club-House", + "Hardcore", + "Terror", + "Indie", + "BritPop", + "Afro-Punk", + "Polsk Punk", + "Beat", + "Christian Gangsta Rap", + "Heavy Metal", + "Black Metal", + "Crossover", + "Contemporary Christian", + "Christian Rock", + "Merengue", + "Salsa", + "Thrash Metal", + "Anime", + "Jpop", + "Synthpop", + // Genres made up by the authors of Winamp (v5.6) but have not been added to the ID3 spec. + "Abstract", + "Art Rock", + "Baroque", + "Bhangra", + "Big beat", + "Breakbeat", + "Chillout", + "Downtempo", + "Dub", + "EBM", + "Eclectic", + "Electro", + "Electroclash", + "Emo", + "Experimental", + "Garage", + "Global", + "IDM", + "Illbient", + "Industro-Goth", + "Jam Band", + "Krautrock", + "Leftfield", + "Lounge", + "Math Rock", + "New Romantic", + "Nu-Breakz", + "Post-Punk", + "Post-Rock", + "Psytrance", + "Shoegaze", + "Space Rock", + "Trop Rock", + "World Music", + "Neoclassical", + "Audiobook", + "Audio theatre", + "Neue Deutsche Welle", + "Podcast", + "Indie-Rock", + "G-Funk", + "Dubstep", + "Garage Rock", + "Psybient" + }; private static final String LANGUAGE_UNDEFINED = "und"; @@ -109,7 +281,6 @@ import java.nio.ByteBuffer; private static final int TYPE_TOP_BYTE_REPLACEMENT = 0xFD; // Truncated value of \uFFFD. private static final String MDTA_KEY_ANDROID_CAPTURE_FPS = "com.android.capture.fps"; - private static final int MDTA_TYPE_INDICATOR_FLOAT = 23; private MetadataUtil() {} @@ -139,15 +310,8 @@ import java.nio.ByteBuffer; Metadata.Entry entry = mdtaMetadata.get(i); if (entry instanceof MdtaMetadataEntry) { MdtaMetadataEntry mdtaMetadataEntry = (MdtaMetadataEntry) entry; - if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key) - && mdtaMetadataEntry.typeIndicator == MDTA_TYPE_INDICATOR_FLOAT) { - try { - float fps = ByteBuffer.wrap(mdtaMetadataEntry.value).asFloatBuffer().get(); - format = format.copyWithFrameRate(fps); - format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); - } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring invalid framerate"); - } + if (MDTA_KEY_ANDROID_CAPTURE_FPS.equals(mdtaMetadataEntry.key)) { + format = format.copyWithMetadata(new Metadata(mdtaMetadataEntry)); } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 6f76a4007..4cc5af89c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -78,7 +77,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private static final int STATE_READING_SAMPLE = 2; /** Brand stored in the ftyp atom for QuickTime media. */ - private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt "); + private static final int BRAND_QUICKTIME = 0x71742020; /** * When seeking within the source, if the offset is greater than or equal to this value (or the @@ -90,7 +89,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { * For poorly interleaved streams, the maximum byte difference one track is allowed to be read * ahead before the source will be reloaded at a new position to read another track. */ - private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 1 * 1024 * 1024; + private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 512 * 1024; private final @Flags int flags; @@ -109,9 +108,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { private ParsableByteArray atomData; private int sampleTrackIndex; + private int sampleBytesRead; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; - private boolean isAc4HeaderRequired; // Extractor outputs. private ExtractorOutput extractorOutput; @@ -159,9 +158,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { containerAtoms.clear(); atomHeaderBytesRead = 0; sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; - isAc4HeaderRequired = false; if (position == 0) { enterReadingAtomHeaderState(); } else if (tracks != null) { @@ -305,13 +304,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; + if (atomSize != atomHeaderBytesRead && atomType == Atom.TYPE_meta) { + maybeSkipRemainingMetaAtomHeaderBytes(input); + } containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { - if (atomType == Atom.TYPE_meta) { - maybeSkipRemainingMetaAtomHeaderBytes(input); - } // Start reading the first child atom. enterReadingAtomHeaderState(); } @@ -502,15 +501,13 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (sampleTrackIndex == C.INDEX_UNSET) { return RESULT_END_OF_INPUT; } - isAc4HeaderRequired = - MimeTypes.AUDIO_AC4.equals(tracks[sampleTrackIndex].track.format.sampleMimeType); } Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; int sampleIndex = track.sampleIndex; long position = track.sampleTable.offsets[sampleIndex]; int sampleSize = track.sampleTable.sizes[sampleIndex]; - long skipAmount = position - inputPosition + sampleBytesWritten; + long skipAmount = position - inputPosition + sampleBytesRead; if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { positionHolder.position = position; return RESULT_SEEK; @@ -538,6 +535,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (sampleCurrentNalBytesRemaining == 0) { // Read the NAL length so that we know where we find the next one. input.readFully(nalLengthData, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + sampleBytesRead += nalUnitLengthFieldLength; nalLength.setPosition(0); int nalLengthInt = nalLength.readInt(); if (nalLengthInt < 0) { @@ -552,21 +550,23 @@ public final class Mp4Extractor implements Extractor, SeekMap { } else { // Write the payload of the NAL unit. int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); + sampleBytesRead += writtenBytes; sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } } } else { - if (isAc4HeaderRequired) { - Ac4Util.getAc4SampleHeader(sampleSize, scratch); - int length = scratch.limit(); - trackOutput.sampleData(scratch, length); - sampleSize += length; - sampleBytesWritten += length; - isAc4HeaderRequired = false; + if (MimeTypes.AUDIO_AC4.equals(track.track.format.sampleMimeType)) { + if (sampleBytesWritten == 0) { + Ac4Util.getAc4SampleHeader(sampleSize, scratch); + trackOutput.sampleData(scratch, Ac4Util.SAMPLE_HEADER_SIZE); + sampleBytesWritten += Ac4Util.SAMPLE_HEADER_SIZE; + } + sampleSize += Ac4Util.SAMPLE_HEADER_SIZE; } while (sampleBytesWritten < sampleSize) { int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesRead += writtenBytes; sampleBytesWritten += writtenBytes; sampleCurrentNalBytesRemaining -= writtenBytes; } @@ -575,6 +575,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleIndex++; sampleTrackIndex = C.INDEX_UNSET; + sampleBytesRead = 0; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; return RESULT_CONTINUE; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index 957c3ba20..b9ecaf174 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -49,7 +49,8 @@ public final class PsshAtomUtil { * @param data The scheme specific data. * @return The PSSH atom. */ - @SuppressWarnings("ParameterNotNullable") + // dereference of possibly-null reference keyId + @SuppressWarnings({"ParameterNotNullable", "nullness:dereference.of.nullable"}) public static byte[] buildPsshAtom( UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { int dataLength = data != null ? data.length : 0; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java index 37049f79c..dac74bfe2 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Sniffer.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.extractor.mp4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -32,32 +31,32 @@ import java.io.IOException; 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("av01"), - 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 - Util.getIntegerCodeForString("dby1"), // Dolby Vision + 0x69736f6d, // isom + 0x69736f32, // iso2 + 0x69736f33, // iso3 + 0x69736f34, // iso4 + 0x69736f35, // iso5 + 0x69736f36, // iso6 + 0x61766331, // avc1 + 0x68766331, // hvc1 + 0x68657631, // hev1 + 0x61763031, // av01 + 0x6d703431, // mp41 + 0x6d703432, // mp42 + 0x33673261, // 3g2a + 0x33673262, // 3g2b + 0x33677236, // 3gr6 + 0x33677336, // 3gs6 + 0x33676536, // 3ge6 + 0x33676736, // 3gg6 + 0x4d345620, // M4V[space] + 0x4d344120, // M4A[space] + 0x66347620, // f4v[space] + 0x6b646469, // kddi + 0x4d345650, // M4VP + 0x71742020, // qt[space][space], Apple QuickTime + 0x4d534e56, // MSNV, Sony PSP + 0x64627931, // dby1, Dolby Vision }; /** @@ -119,10 +118,6 @@ import java.io.IOException; } } - if (inputLength != C.LENGTH_UNSET && bytesSearched + atomSize > inputLength + 10) { //added small trashhold for buggy files - // The file is invalid because the atom extends past the end of the file. - return false; - } if (atomSize < headerSize) { // The file is invalid because the atom size is too small for its header. return false; @@ -188,7 +183,7 @@ import java.io.IOException; */ private static boolean isCompatibleBrand(int brand) { // Accept all brands starting '3gp'. - if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) { + if (brand >>> 8 == 0x00336770) { return true; } for (int compatibleBrand : COMPATIBLE_BRANDS) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java index 7676926c4..0a21ddd3a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/Track.java @@ -129,6 +129,8 @@ public final class Track { : sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; } + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") public Track copyWithFormat(Format format) { return new Track( id, diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java index 51ec2bf28..0272e8e33 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java @@ -60,14 +60,10 @@ import java.io.IOException; * The size of each sample in the fragment. */ public int[] sampleSizeTable; - /** - * The composition time offset of each sample in the fragment. - */ - public int[] sampleCompositionTimeOffsetTable; - /** - * The decoding time of each sample in the fragment. - */ - public long[] sampleDecodingTimeTable; + /** The composition time offset of each sample in the fragment, in microseconds. */ + public int[] sampleCompositionTimeOffsetUsTable; + /** The decoding time of each sample in the fragment, in microseconds. */ + public long[] sampleDecodingTimeUsTable; /** * Indicates which samples are sync frames. */ @@ -139,8 +135,8 @@ import java.io.IOException; // 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]; + sampleCompositionTimeOffsetUsTable = new int[tableSize]; + sampleDecodingTimeUsTable = new long[tableSize]; sampleIsSyncFrameTable = new boolean[tableSize]; sampleHasSubsampleEncryptionTable = new boolean[tableSize]; } @@ -186,8 +182,14 @@ import java.io.IOException; sampleEncryptionDataNeedsFill = false; } - public long getSamplePresentationTime(int index) { - return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; + /** + * Returns the sample presentation timestamp in microseconds. + * + * @param index The sample index. + * @return The presentation timestamps of this sample in microseconds. + */ + public long getSamplePresentationTimeUs(int index) { + return sampleDecodingTimeUsTable[index] + sampleCompositionTimeOffsetUsTable[index]; } /** Returns whether the sample at the given index has a subsample encryption table. */ diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java index 4efd5c5e1..f99b2420c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java @@ -15,18 +15,18 @@ */ package com.google.android.exoplayer2.extractor.ogg; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.FlacFrameReader; +import com.google.android.exoplayer2.extractor.FlacMetadataReader; +import com.google.android.exoplayer2.extractor.FlacSeekTableSeekMap; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.SeekPoint; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.FlacConstants; import com.google.android.exoplayer2.util.FlacStreamMetadata; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; 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. @@ -34,7 +34,6 @@ import java.util.List; /* package */ final class FlacReader extends StreamReader { private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; - private static final byte SEEKTABLE_PACKET_TYPE = 0x03; private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; @@ -68,30 +67,17 @@ import java.util.List; } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) - throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) { byte[] data = packet.data; if (streamMetadata == null) { streamMetadata = new FlacStreamMetadata(data, 17); byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit()); - metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks - List initializationData = Collections.singletonList(metadata); - setupData.format = - Format.createAudioSampleFormat( - null, - MimeTypes.AUDIO_FLAC, - null, - Format.NO_VALUE, - streamMetadata.bitRate(), - streamMetadata.channels, - streamMetadata.sampleRate, - initializationData, - null, - 0, - null); - } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) { + setupData.format = streamMetadata.getFormat(metadata, /* id3Metadata= */ null); + } else if ((data[0] & 0x7F) == FlacConstants.METADATA_TYPE_SEEK_TABLE) { flacOggSeeker = new FlacOggSeeker(); - flacOggSeeker.parseSeekTable(packet); + FlacStreamMetadata.SeekTable seekTable = + FlacMetadataReader.readSeekTableMetadataBlock(packet); + streamMetadata = streamMetadata.copyWithSeekTable(seekTable); } else if (isAudioPacket(data)) { if (flacOggSeeker != null) { flacOggSeeker.setFirstFrameOffset(position); @@ -103,44 +89,19 @@ import java.util.List; } private int getFlacFrameBlockSize(ParsableByteArray packet) { - int blockSizeCode = (packet.data[2] & 0xFF) >> 4; - switch (blockSizeCode) { - case 1: - return 192; - case 2: - case 3: - case 4: - case 5: - return 576 << (blockSizeCode - 2); - case 6: - case 7: - // skip the sample number - packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); - packet.readUtf8EncodedLong(); - int value = blockSizeCode == 6 ? packet.readUnsignedByte() : packet.readUnsignedShort(); - packet.setPosition(0); - return value + 1; - case 8: - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - return 256 << (blockSizeCode - 8); - default: - return -1; + int blockSizeKey = (packet.data[2] & 0xFF) >> 4; + if (blockSizeKey == 6 || blockSizeKey == 7) { + // Skip the sample number. + packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); + packet.readUtf8EncodedLong(); } + int result = FlacFrameReader.readFrameBlockSizeSamplesFromKey(packet, blockSizeKey); + packet.setPosition(0); + return result; } - private class FlacOggSeeker implements OggSeeker, SeekMap { + private class FlacOggSeeker implements OggSeeker { - private static final int METADATA_LENGTH_OFFSET = 1; - private static final int SEEK_POINT_SIZE = 18; - - private long[] seekPointGranules; - private long[] seekPointOffsets; private long firstFrameOffset; private long pendingSeekGranule; @@ -153,27 +114,6 @@ import java.util.List; this.firstFrameOffset = firstFrameOffset; } - /** - * Parses a FLAC file seek table metadata structure and initializes internal fields. - * - * @param data A {@link ParsableByteArray} including whole seek table metadata block. Its - * position should be set to the beginning of the block. - * @see FLAC format - * METADATA_BLOCK_SEEKTABLE - */ - public void parseSeekTable(ParsableByteArray data) { - data.skipBytes(METADATA_LENGTH_OFFSET); - int length = data.readUnsignedInt24(); - int numberOfSeekPoints = length / SEEK_POINT_SIZE; - seekPointGranules = new long[numberOfSeekPoints]; - seekPointOffsets = new long[numberOfSeekPoints]; - for (int i = 0; i < numberOfSeekPoints; i++) { - seekPointGranules[i] = data.readLong(); - seekPointOffsets[i] = data.readLong(); - data.skipBytes(2); // Skip "Number of samples in the target frame." - } - } - @Override public long read(ExtractorInput input) throws IOException, InterruptedException { if (pendingSeekGranule >= 0) { @@ -186,40 +126,16 @@ import java.util.List; @Override public void startSeek(long targetGranule) { + Assertions.checkNotNull(streamMetadata.seekTable); + long[] seekPointGranules = streamMetadata.seekTable.pointSampleNumbers; int index = Util.binarySearchFloor(seekPointGranules, targetGranule, true, true); pendingSeekGranule = seekPointGranules[index]; } @Override public SeekMap createSeekMap() { - return this; - } - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - long granule = convertTimeToGranule(timeUs); - int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); - long seekTimeUs = convertGranuleToTime(seekPointGranules[index]); - long seekPosition = firstFrameOffset + seekPointOffsets[index]; - SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); - if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) { - return new SeekPoints(seekPoint); - } else { - long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]); - long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1]; - SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); - return new SeekPoints(seekPoint, secondSeekPoint); - } - } - - @Override - public long getDurationUs() { - return streamMetadata.durationUs(); + Assertions.checkState(firstFrameOffset != -1); + return new FlacSeekTableSeekMap(streamMetadata, firstFrameOffset); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java index bb84909f6..c7fb3ff6a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; @@ -34,7 +33,7 @@ import java.io.IOException; public static final int MAX_PAGE_SIZE = EMPTY_PAGE_HEADER_SIZE + MAX_SEGMENT_COUNT + MAX_PAGE_PAYLOAD; - private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS"); + private static final int TYPE_OGGS = 0x4f676753; public int revision; public int type; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java index ff5f11557..90ae3f0f4 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -38,7 +37,7 @@ import java.util.List; */ private static final int SAMPLE_RATE = 48000; - private static final int OPUS_CODE = Util.getIntegerCodeForString("Opus"); + private static final int OPUS_CODE = 0x4f707573; private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}; private boolean headerRead; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java index 2675edd5b..b57678266 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java @@ -18,7 +18,8 @@ package com.google.android.exoplayer2.extractor.ogg; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.ogg.VorbisUtil.Mode; +import com.google.android.exoplayer2.extractor.VorbisUtil; +import com.google.android.exoplayer2.extractor.VorbisUtil.Mode; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java index aa77aba30..3d7627624 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -35,7 +34,7 @@ public final class RawCcExtractor implements Extractor { private static final int SCRATCH_SIZE = 9; private static final int HEADER_SIZE = 8; - private static final int HEADER_ID = Util.getIntegerCodeForString("RCC\u0001"); + private static final int HEADER_ID = 0x52434301; private static final int TIMESTAMP_SIZE_V0 = 4; private static final int TIMESTAMP_SIZE_V1 = 8; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java index 889a49755..b1d15b718 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.Ac3Util; @@ -27,7 +29,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** @@ -45,20 +46,14 @@ public final class Ac3Extractor implements Extractor { private static final int MAX_SNIFF_BYTES = 8 * 1024; private static final int AC3_SYNC_WORD = 0x0B77; private static final int MAX_SYNC_FRAME_SIZE = 2786; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - private final long firstSampleTimestampUs; private final Ac3Reader reader; private final ParsableByteArray sampleData; private boolean startedPacket; + /** Creates a new extractor for AC-3 bitstreams. */ public Ac3Extractor() { - this(0); - } - - public Ac3Extractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; reader = new Ac3Reader(); sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } @@ -68,10 +63,10 @@ public final class Ac3Extractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; while (true) { - input.peekFully(scratch.data, 0, 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; @@ -142,7 +137,7 @@ public final class Ac3Extractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java index 133c0f368..205d71e16 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Ac4Extractor.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.audio.Ac4Util.AC40_SYNCWORD; import static com.google.android.exoplayer2.audio.Ac4Util.AC41_SYNCWORD; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.audio.Ac4Util; @@ -29,7 +31,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Extracts data from AC-4 bitstreams. */ @@ -53,9 +54,6 @@ public final class Ac4Extractor implements Extractor { /** The size of the frame header, in bytes. */ private static final int FRAME_HEADER_SIZE = 7; - private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); - - private final long firstSampleTimestampUs; private final Ac4Reader reader; private final ParsableByteArray sampleData; @@ -63,12 +61,6 @@ public final class Ac4Extractor implements Extractor { /** Creates a new extractor for AC-4 bitstreams. */ public Ac4Extractor() { - this(/* firstSampleTimestampUs= */ 0); - } - - /** Creates a new extractor for AC-4 bitstreams, using the specified first sample timestamp. */ - public Ac4Extractor(long firstSampleTimestampUs) { - this.firstSampleTimestampUs = firstSampleTimestampUs; reader = new Ac4Reader(); sampleData = new ParsableByteArray(READ_BUFFER_SIZE); } @@ -78,10 +70,10 @@ public final class Ac4Extractor implements Extractor { @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. - ParsableByteArray scratch = new ParsableByteArray(10); + ParsableByteArray scratch = new ParsableByteArray(ID3_HEADER_LENGTH); int startPosition = 0; while (true) { - input.peekFully(scratch.data, /* offset= */ 0, /* length= */ 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; @@ -153,7 +145,7 @@ public final class Ac4Extractor implements Extractor { if (!startedPacket) { // Pass data to the reader as though it's contained within a single infinitely long packet. - reader.packetStarted(firstSampleTimestampUs, FLAG_DATA_ALIGNMENT_INDICATOR); + reader.packetStarted(/* pesTimeUs= */ 0, FLAG_DATA_ALIGNMENT_INDICATOR); startedPacket = true; } // TODO: Make it possible for the reader to consume the dataSource directly, so that it becomes diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java index 9526a6576..86dacd8c3 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_TAG; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -32,7 +34,7 @@ import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerat import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; +import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -66,7 +68,6 @@ public final class AdtsExtractor implements Extractor { public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; private static final int MAX_PACKET_SIZE = 2 * 1024; - 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. @@ -84,9 +85,8 @@ public final class AdtsExtractor implements Extractor { private final ParsableByteArray packetBuffer; private final ParsableByteArray scratch; private final ParsableBitArray scratchBits; - private final long firstStreamSampleTimestampUs; - private @Nullable ExtractorOutput extractorOutput; + @Nullable private ExtractorOutput extractorOutput; private long firstSampleTimestampUs; private long firstFramePosition; @@ -95,28 +95,24 @@ public final class AdtsExtractor implements Extractor { private boolean startedPacket; private boolean hasOutputSeekMap; + /** Creates a new extractor for ADTS bitstreams. */ public AdtsExtractor() { - this(0); - } - - public AdtsExtractor(long firstStreamSampleTimestampUs) { - this(/* firstStreamSampleTimestampUs= */ firstStreamSampleTimestampUs, /* flags= */ 0); + this(/* flags= */ 0); } /** - * @param firstStreamSampleTimestampUs The timestamp to be used for the first sample of the stream - * output from this extractor. + * Creates a new extractor for ADTS bitstreams. + * * @param flags Flags that control the extractor's behavior. */ - public AdtsExtractor(long firstStreamSampleTimestampUs, @Flags int flags) { - this.firstStreamSampleTimestampUs = firstStreamSampleTimestampUs; - this.firstSampleTimestampUs = firstStreamSampleTimestampUs; + public AdtsExtractor(@Flags int flags) { this.flags = flags; reader = new AdtsReader(true); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); averageFrameSize = C.LENGTH_UNSET; firstFramePosition = C.POSITION_UNSET; - scratch = new ParsableByteArray(10); + // Allocate scratch space for an ID3 header. The same buffer is also used to read 4 byte values. + scratch = new ParsableByteArray(ID3_HEADER_LENGTH); scratchBits = new ParsableBitArray(scratch.data); } @@ -173,7 +169,7 @@ public final class AdtsExtractor implements Extractor { public void seek(long position, long timeUs) { startedPacket = false; reader.seek(); - firstSampleTimestampUs = firstStreamSampleTimestampUs + timeUs; + firstSampleTimestampUs = timeUs; } @Override @@ -216,14 +212,14 @@ public final class AdtsExtractor implements Extractor { private int peekId3Header(ExtractorInput input) throws IOException, InterruptedException { int firstFramePosition = 0; while (true) { - input.peekFully(scratch.data, 0, 10); + input.peekFully(scratch.data, /* offset= */ 0, ID3_HEADER_LENGTH); scratch.setPosition(0); if (scratch.readUnsignedInt24() != ID3_TAG) { break; } scratch.skipBytes(3); int length = scratch.readSynchSafeInt(); - firstFramePosition += 10 + length; + firstFramePosition += ID3_HEADER_LENGTH + length; input.advancePeekPosition(length); } input.resetPeekPosition(); @@ -271,36 +267,43 @@ public final class AdtsExtractor implements Extractor { int numValidFrames = 0; long totalValidFramesSize = 0; - while (input.peekFully( - scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { - scratch.setPosition(0); - int syncBytes = scratch.readUnsignedShort(); - if (!AdtsReader.isAdtsSyncWord(syncBytes)) { - // Invalid sync byte pattern. - // Constant bit-rate seeking will probably fail for this stream. - numValidFrames = 0; - break; - } else { - // Read the frame size. - if (!input.peekFully( - scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { - break; - } - scratchBits.setPosition(14); - int currentFrameSize = scratchBits.readBits(13); - // Either the stream is malformed OR we're not parsing an ADTS stream. - if (currentFrameSize <= 6) { - hasCalculatedAverageFrameSize = true; - throw new ParserException("Malformed ADTS stream"); - } - totalValidFramesSize += currentFrameSize; - if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { - break; - } - if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + try { + while (input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 2, /* allowEndOfInput= */ true)) { + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if (!AdtsReader.isAdtsSyncWord(syncBytes)) { + // Invalid sync byte pattern. + // Constant bit-rate seeking will probably fail for this stream. + numValidFrames = 0; break; + } else { + // Read the frame size. + if (!input.peekFully( + scratch.data, /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true)) { + break; + } + scratchBits.setPosition(14); + int currentFrameSize = scratchBits.readBits(13); + // Either the stream is malformed OR we're not parsing an ADTS stream. + if (currentFrameSize <= 6) { + hasCalculatedAverageFrameSize = true; + throw new ParserException("Malformed ADTS stream"); + } + totalValidFramesSize += currentFrameSize; + if (++numValidFrames == NUM_FRAMES_FOR_AVERAGE_FRAME_SIZE) { + break; + } + if (!input.advancePeekPosition(currentFrameSize - 6, /* allowEndOfInput= */ true)) { + break; + } } } + } catch (EOFException e) { + // We reached the end of the input during a peekFully() or advancePeekPosition() operation. + // This is OK, it just means the input has an incomplete ADTS frame at the end. Ideally + // ExtractorInput would allow these operations to encounter end-of-input without throwing an + // exception [internal: b/145586657]. } input.resetPeekPosition(); if (numValidFrames > 0) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 589b54317..bde575f39 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -345,42 +345,44 @@ public final class AdtsReader implements ElementaryStreamReader { } /** - * Returns whether the given syncPositionCandidate is a real SYNC word. - * - *

SYNC word pattern can occur within AAC data, so we perform a few checks to make sure this is - * really a SYNC word. This includes: + * Checks whether a candidate SYNC word position is likely to be the position of a real SYNC word. + * The caller must check that the first byte of the SYNC word is 0xFF before calling this method. + * This method performs the following checks: * *

    - *
  • Checking if MPEG version of this frame matches the first detected version. - *
  • Checking if the sample rate index of this frame matches the first detected sample rate - * index. - *
  • Checking if the bytes immediately after the current package also match a SYNC-word. + *
  • The MPEG version of this frame must match the previously detected version. + *
  • The sample rate index of this frame must match the previously detected sample rate index. + *
  • The frame size must be at least 7 bytes + *
  • The bytes following the frame must be either another SYNC word with the same MPEG + * version, or the start of an ID3 header. *
* - * If the buffer runs out of data for any check, optimistically skip that check, because - * AdtsReader consumes each buffer as a whole. We will still run a header validity check later. + * With the exception of the first check, if there is insufficient data in the buffer then checks + * are optimistically skipped and {@code true} is returned. + * + * @param pesBuffer The buffer containing at data to check. + * @param syncPositionCandidate The candidate SYNC word position. May be -1 if the first byte of + * the candidate was the last byte of the previously consumed buffer. + * @return True if all checks were passed or skipped, indicating the position is likely to be the + * position of a real SYNC word. False otherwise. */ private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) { - // The SYNC word contains 2 bytes, and the first byte may be in the previously consumed buffer. - // Hence the second byte of the SYNC word may be byte 0 of this buffer, and - // syncPositionCandidate (which indicates position of the first byte of the SYNC word) may be - // -1. - // Since the first byte of the SYNC word is always FF, which does not contain any informational - // bits, we set the byte position to be the second byte in the SYNC word to ensure it's always - // within this buffer. pesBuffer.setPosition(syncPositionCandidate + 1); if (!tryRead(pesBuffer, adtsScratch.data, 1)) { return false; } + // The MPEG version of this frame must match the previously detected version. adtsScratch.setPosition(4); int currentFrameVersion = adtsScratch.readBits(1); if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) { return false; } + // The sample rate index of this frame must match the previously detected sample rate index. if (firstFrameSampleRateIndex != C.INDEX_UNSET) { if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + // Insufficient data for further checks. return true; } adtsScratch.setPosition(2); @@ -391,24 +393,50 @@ public final class AdtsReader implements ElementaryStreamReader { pesBuffer.setPosition(syncPositionCandidate + 2); } - // Optionally check the byte after this frame matches SYNC word. - + // The frame size must be at least 7 bytes. if (!tryRead(pesBuffer, adtsScratch.data, 4)) { + // Insufficient data for further checks. return true; } adtsScratch.setPosition(14); int frameSize = adtsScratch.readBits(13); - if (frameSize <= 6) { - // Not a frame. + if (frameSize < 7) { return false; } + + // The bytes following the frame must be either another SYNC word with the same MPEG version, or + // the start of an ID3 header. + byte[] data = pesBuffer.data; + int dataLimit = pesBuffer.limit(); int nextSyncPosition = syncPositionCandidate + frameSize; - if (nextSyncPosition + 1 >= pesBuffer.limit()) { + if (nextSyncPosition >= dataLimit) { + // Insufficient data for further checks. return true; } - return (isAdtsSyncBytes(pesBuffer.data[nextSyncPosition], pesBuffer.data[nextSyncPosition + 1]) - && (firstFrameVersion == VERSION_UNSET - || ((pesBuffer.data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion)); + if (data[nextSyncPosition] == (byte) 0xFF) { + if (nextSyncPosition + 1 == dataLimit) { + // Insufficient data for further checks. + return true; + } + return isAdtsSyncBytes((byte) 0xFF, data[nextSyncPosition + 1]) + && ((data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion; + } else { + if (data[nextSyncPosition] != 'I') { + return false; + } + if (nextSyncPosition + 1 == dataLimit) { + // Insufficient data for further checks. + return true; + } + if (data[nextSyncPosition + 1] != 'D') { + return false; + } + if (nextSyncPosition + 2 == dataLimit) { + // Insufficient data for further checks. + return true; + } + return data[nextSyncPosition + 2] == '3'; + } } private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 15c37430b..24d17f495 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.annotation.IntDef; import android.util.SparseArray; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.text.cea.Cea708InitializationData; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java index 1564157d4..e7f2c1935 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/H262Reader.java @@ -72,7 +72,7 @@ public final class H262Reader implements ElementaryStreamReader { this(null); } - public H262Reader(UserDataReader userDataReader) { + /* package */ H262Reader(UserDataReader userDataReader) { this.userDataReader = userDataReader; prefixFlags = new boolean[4]; csdBuffer = new CsdBuffer(128); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java index 88bde5374..b4007ea4a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/H265Reader.java @@ -41,6 +41,7 @@ public final class H265Reader implements ElementaryStreamReader { 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 AUD_NUT = 35; private static final int PREFIX_SEI_NUT = 39; private static final int SUFFIX_SEI_NUT = 40; @@ -59,7 +60,7 @@ public final class H265Reader implements ElementaryStreamReader { private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer prefixSei; - private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed? + private final NalUnitTargetBuffer suffixSei; private long totalBytesWritten; // Per packet state that gets reset at the start of each packet. @@ -161,9 +162,8 @@ public final class H265Reader implements ElementaryStreamReader { } private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { - if (hasOutputFormat) { - sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); - } else { + sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs, hasOutputFormat); + if (!hasOutputFormat) { vps.startNalUnit(nalUnitType); sps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType); @@ -173,9 +173,8 @@ public final class H265Reader implements ElementaryStreamReader { } private void nalUnitData(byte[] dataArray, int offset, int limit) { - if (hasOutputFormat) { - sampleReader.readNalUnitData(dataArray, offset, limit); - } else { + sampleReader.readNalUnitData(dataArray, offset, limit); + if (!hasOutputFormat) { vps.appendToNalUnit(dataArray, offset, limit); sps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit); @@ -185,9 +184,8 @@ public final class H265Reader implements ElementaryStreamReader { } private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { - if (hasOutputFormat) { - sampleReader.endNalUnit(position, offset); - } else { + sampleReader.endNalUnit(position, offset, hasOutputFormat); + if (!hasOutputFormat) { vps.endNalUnit(discardPadding); sps.endNalUnit(discardPadding); pps.endNalUnit(discardPadding); @@ -400,17 +398,17 @@ public final class H265Reader implements ElementaryStreamReader { private final TrackOutput output; // Per NAL unit state. A sample consists of one or more NAL units. - private long nalUnitStartPosition; + private long nalUnitPosition; private boolean nalUnitHasKeyframeData; private int nalUnitBytesRead; private long nalUnitTimeUs; private boolean lookingForFirstSliceFlag; private boolean isFirstSlice; - private boolean isFirstParameterSet; + private boolean isFirstPrefixNalUnit; // Per sample state that gets reset at the start of each sample. private boolean readingSample; - private boolean writingParameterSets; + private boolean readingPrefix; private long samplePosition; private long sampleTimeUs; private boolean sampleIsKeyframe; @@ -422,32 +420,33 @@ public final class H265Reader implements ElementaryStreamReader { public void reset() { lookingForFirstSliceFlag = false; isFirstSlice = false; - isFirstParameterSet = false; + isFirstPrefixNalUnit = false; readingSample = false; - writingParameterSets = false; + readingPrefix = false; } - public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { + public void startNalUnit( + long position, int offset, int nalUnitType, long pesTimeUs, boolean hasOutputFormat) { isFirstSlice = false; - isFirstParameterSet = false; + isFirstPrefixNalUnit = false; nalUnitTimeUs = pesTimeUs; nalUnitBytesRead = 0; - nalUnitStartPosition = position; + nalUnitPosition = position; - if (nalUnitType >= VPS_NUT) { - if (!writingParameterSets && readingSample) { - // This is a non-VCL NAL unit, so flush the previous sample. - outputSample(offset); + if (!isVclBodyNalUnit(nalUnitType)) { + if (readingSample && !readingPrefix) { + if (hasOutputFormat) { + outputSample(offset); + } readingSample = false; } - if (nalUnitType <= PPS_NUT) { - // This sample will have parameter sets at the start. - isFirstParameterSet = !writingParameterSets; - writingParameterSets = true; + if (isPrefixNalUnit(nalUnitType)) { + isFirstPrefixNalUnit = !readingPrefix; + readingPrefix = true; } } - // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp. + // Look for the first slice flag if this NAL unit contains a slice_segment_layer_rbsp. nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT); lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R; } @@ -464,31 +463,39 @@ public final class H265Reader implements ElementaryStreamReader { } } - public void endNalUnit(long position, int offset) { - if (writingParameterSets && isFirstSlice) { + public void endNalUnit(long position, int offset, boolean hasOutputFormat) { + if (readingPrefix && isFirstSlice) { // This sample has parameter sets. Reset the key-frame flag based on the first slice. sampleIsKeyframe = nalUnitHasKeyframeData; - writingParameterSets = false; - } else if (isFirstParameterSet || isFirstSlice) { + readingPrefix = false; + } else if (isFirstPrefixNalUnit || isFirstSlice) { // This NAL unit is at the start of a new sample (access unit). - if (readingSample) { + if (hasOutputFormat && readingSample) { // Output the sample ending before this NAL unit. - int nalUnitLength = (int) (position - nalUnitStartPosition); + int nalUnitLength = (int) (position - nalUnitPosition); outputSample(offset + nalUnitLength); } - samplePosition = nalUnitStartPosition; + samplePosition = nalUnitPosition; sampleTimeUs = nalUnitTimeUs; - readingSample = true; sampleIsKeyframe = nalUnitHasKeyframeData; + readingSample = true; } } private void outputSample(int offset) { @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; - int size = (int) (nalUnitStartPosition - samplePosition); + int size = (int) (nalUnitPosition - samplePosition); output.sampleMetadata(sampleTimeUs, flags, size, offset, null); } - } + /** Returns whether a NAL unit type is one that occurs before any VCL NAL units in a sample. */ + private static boolean isPrefixNalUnit(int nalUnitType) { + return (VPS_NUT <= nalUnitType && nalUnitType <= AUD_NUT) || nalUnitType == PREFIX_SEI_NUT; + } + /** Returns whether a NAL unit type is one that occurs in the VLC body of a sample. */ + private static boolean isVclBodyNalUnit(int nalUnitType) { + return nalUnitType < VPS_NUT || nalUnitType == SUFFIX_SEI_NUT; + } + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java index f936fb9e4..77ec48d0a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/Id3Reader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_DATA_ALIGNMENT_INDICATOR; +import static com.google.android.exoplayer2.metadata.id3.Id3Decoder.ID3_HEADER_LENGTH; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -33,8 +34,6 @@ public final class Id3Reader implements ElementaryStreamReader { private static final String TAG = "Id3Reader"; - private static final int ID3_HEADER_SIZE = 10; - private final ParsableByteArray id3Header; private TrackOutput output; @@ -48,7 +47,7 @@ public final class Id3Reader implements ElementaryStreamReader { private int sampleBytesRead; public Id3Reader() { - id3Header = new ParsableByteArray(ID3_HEADER_SIZE); + id3Header = new ParsableByteArray(ID3_HEADER_LENGTH); } @Override @@ -81,12 +80,12 @@ public final class Id3Reader implements ElementaryStreamReader { return; } int bytesAvailable = data.bytesLeft(); - if (sampleBytesRead < ID3_HEADER_SIZE) { + if (sampleBytesRead < ID3_HEADER_LENGTH) { // We're still reading the ID3 header. - int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead); + int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_LENGTH - sampleBytesRead); System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead, headerBytesAvailable); - if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) { + if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_LENGTH) { // We've finished reading the ID3 header. Extract the sample size. id3Header.setPosition(0); if ('I' != id3Header.readUnsignedByte() || 'D' != id3Header.readUnsignedByte() @@ -96,7 +95,7 @@ public final class Id3Reader implements ElementaryStreamReader { return; } id3Header.skipBytes(3); // version (2) + flags (1) - sampleSize = ID3_HEADER_SIZE + id3Header.readSynchSafeInt(); + sampleSize = ID3_HEADER_LENGTH + id3Header.readSynchSafeInt(); } } // Write data to the output. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index 39e74ae6a..4ad9adfa2 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java index 4efd38b7e..c4f53ba17 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/PsBinarySearchSeeker.java @@ -69,8 +69,7 @@ import java.io.IOException; } @Override - public TimestampSearchResult searchForTimestamp( - ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) throws IOException, InterruptedException { long inputPosition = input.getPosition(); int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index f453a9cc4..fec108fd5 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -168,8 +168,7 @@ public final class PsExtractor implements Extractor { } maybeOutputSeekMap(inputLength); if (psBinarySearchSeeker != null && psBinarySearchSeeker.isSeeking()) { - return psBinarySearchSeeker.handlePendingSeek( - input, seekPosition, /* outputFrameHolder= */ null); + return psBinarySearchSeeker.handlePendingSeek(input, seekPosition); } input.resetPeekPosition(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java index 101a1f74d..bc590c9d4 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/SectionReader.java @@ -114,7 +114,7 @@ public final class SectionReader implements TsPayloadReader { if (bytesRead == totalSectionLength) { if (sectionSyntaxIndicator) { // This section has common syntax as defined in ISO/IEC 13818-1, section 2.4.4.11. - if (Util.crc(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { + if (Util.crc32(sectionData.data, 0, totalSectionLength, 0xFFFFFFFF) != 0) { // The CRC is invalid so discard the section. waitingForPayloadStart = true; return; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java index 895c22469..d032ef588 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/SeiReader.java @@ -26,10 +26,8 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; -/** - * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. - */ -/* package */ final class SeiReader { +/** Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}. */ +public final class SeiReader { private final List closedCaptionFormats; private final TrackOutput[] outputs; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java index ea2519d2e..a627c00ba 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java @@ -73,8 +73,7 @@ import java.io.IOException; } @Override - public TimestampSearchResult searchForTimestamp( - ExtractorInput input, long targetTimestamp, OutputFrameHolder outputFrameHolder) + public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) throws IOException, InterruptedException { long inputPosition = input.getPosition(); int bytesToSearch = (int) Math.min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index a2f8568cb..2bd5b1255 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.android.exoplayer2.extractor.ts.TsPayloadReader.FLAG_PAYLOAD_UNIT_START_INDICATOR; -import androidx.annotation.IntDef; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; @@ -101,10 +101,10 @@ public final class TsExtractor implements Extractor { private static final int TS_PAT_PID = 0; private static final int MAX_PID_PLUS_ONE = 0x2000; - 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 AC4_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-4"); - private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC"); + private static final long AC3_FORMAT_IDENTIFIER = 0x41432d33; + private static final long E_AC3_FORMAT_IDENTIFIER = 0x45414333; + private static final long AC4_FORMAT_IDENTIFIER = 0x41432d34; + private static final long HEVC_FORMAT_IDENTIFIER = 0x48455643; private static final int BUFFER_SIZE = TS_PACKET_SIZE * 50; private static final int SNIFF_TS_PACKET_COUNT = 5; @@ -268,8 +268,7 @@ public final class TsExtractor implements Extractor { } if (tsBinarySearchSeeker != null && tsBinarySearchSeeker.isSeeking()) { - return tsBinarySearchSeeker.handlePendingSeek( - input, seekPosition, /* outputFrameHolder= */ null); + return tsBinarySearchSeeker.handlePendingSeek(input, seekPosition); } } @@ -461,10 +460,15 @@ public final class TsExtractor implements Extractor { // See ISO/IEC 13818-1, section 2.4.4.4 for more information on table id assignment. return; } - // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), - // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), - // section_number (8), last_section_number (8) - sectionData.skipBytes(7); + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(4) + int secondHeaderByte = sectionData.readUnsignedByte(); + if ((secondHeaderByte & 0x80) == 0) { + // section_syntax_indicator must be 1. See ISO/IEC 13818-1, section 2.4.4.5. + return; + } + // section_length(8), transport_stream_id (16), reserved (2), version_number (5), + // current_next_indicator (1), section_number (8), last_section_number (8) + sectionData.skipBytes(6); int programCount = sectionData.bytesLeft() / 4; for (int i = 0; i < programCount; i++) { @@ -536,8 +540,14 @@ public final class TsExtractor implements Extractor { timestampAdjusters.add(timestampAdjuster); } - // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12) - sectionData.skipBytes(2); + // section_syntax_indicator(1), '0'(1), reserved(2), section_length(4) + int secondHeaderByte = sectionData.readUnsignedByte(); + if ((secondHeaderByte & 0x80) == 0) { + // section_syntax_indicator must be 1. See ISO/IEC 13818-1, section 2.4.4.9. + return; + } + // section_length(8) + sectionData.skipBytes(1); int programNumber = sectionData.readUnsignedShort(); // Skip 3 bytes (24 bits), including: diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index 536a31c9f..af2723525 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.extractor.ts; -import androidx.annotation.IntDef; import android.util.SparseArray; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 91097c9e5..dd2729424 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -15,9 +15,11 @@ */ package com.google.android.exoplayer2.extractor.wav; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.WavUtil; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -26,24 +28,37 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Extracts data from WAV byte streams. */ public final class WavExtractor implements Extractor { + /** + * When outputting PCM data to a {@link TrackOutput}, we can choose how many frames are grouped + * into each sample, and hence each sample's duration. This is the target number of samples to + * output for each second of media, meaning that each sample will have a duration of ~100ms. + */ + private static final int TARGET_SAMPLES_PER_SECOND = 10; + /** Factory for {@link WavExtractor} instances. */ public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new WavExtractor()}; - /** 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; + @MonotonicNonNull private ExtractorOutput extractorOutput; + @MonotonicNonNull private TrackOutput trackOutput; + @MonotonicNonNull private OutputWriter outputWriter; + private int dataStartPosition; + private long dataEndPosition; - private ExtractorOutput extractorOutput; - private TrackOutput trackOutput; - private WavHeader wavHeader; - private int bytesPerFrame; - private int pendingBytes; + public WavExtractor() { + dataStartPosition = C.POSITION_UNSET; + dataEndPosition = C.POSITION_UNSET; + } @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { @@ -54,13 +69,14 @@ public final class WavExtractor implements Extractor { public void init(ExtractorOutput output) { extractorOutput = output; trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); - wavHeader = null; output.endTracks(); } @Override public void seek(long position, long timeUs) { - pendingBytes = 0; + if (outputWriter != null) { + outputWriter.reset(timeUs); + } } @Override @@ -71,50 +87,476 @@ public final class WavExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { - if (wavHeader == null) { - wavHeader = WavHeaderReader.peek(input); - if (wavHeader == null) { + assertInitialized(); + if (outputWriter == null) { + WavHeader header = WavHeaderReader.peek(input); + if (header == null) { // Should only happen if the media wasn't sniffed. throw new ParserException("Unsupported or unrecognized wav header."); } - Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, - wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(), - wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, 0, null); - trackOutput.format(format); - bytesPerFrame = wavHeader.getBytesPerFrame(); + + if (header.formatType == WavUtil.TYPE_IMA_ADPCM) { + outputWriter = new ImaAdPcmOutputWriter(extractorOutput, trackOutput, header); + } else if (header.formatType == WavUtil.TYPE_ALAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_ALAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else if (header.formatType == WavUtil.TYPE_MLAW) { + outputWriter = + new PassthroughOutputWriter( + extractorOutput, + trackOutput, + header, + MimeTypes.AUDIO_MLAW, + /* pcmEncoding= */ Format.NO_VALUE); + } else { + @C.PcmEncoding + int pcmEncoding = WavUtil.getPcmEncodingForType(header.formatType, header.bitsPerSample); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported WAV format type: " + header.formatType); + } + outputWriter = + new PassthroughOutputWriter( + extractorOutput, trackOutput, header, MimeTypes.AUDIO_RAW, pcmEncoding); + } } - if (!wavHeader.hasDataBounds()) { - WavHeaderReader.skipToData(input, wavHeader); - extractorOutput.seekMap(wavHeader); + if (dataStartPosition == C.POSITION_UNSET) { + Pair dataBounds = WavHeaderReader.skipToData(input); + dataStartPosition = dataBounds.first.intValue(); + dataEndPosition = dataBounds.second; + outputWriter.init(dataStartPosition, dataEndPosition); } else if (input.getPosition() == 0) { - input.skipFully(wavHeader.getDataStartPosition()); + input.skipFully(dataStartPosition); } - long dataEndPosition = wavHeader.getDataEndPosition(); Assertions.checkState(dataEndPosition != C.POSITION_UNSET); - long bytesLeft = dataEndPosition - input.getPosition(); - if (bytesLeft <= 0) { - return Extractor.RESULT_END_OF_INPUT; - } - - int maxBytesToRead = (int) Math.min(MAX_INPUT_SIZE - pendingBytes, bytesLeft); - int bytesAppended = trackOutput.sampleData(input, maxBytesToRead, true); - if (bytesAppended != RESULT_END_OF_INPUT) { - pendingBytes += bytesAppended; - } - - // Samples must consist of a whole number of frames. - int pendingFrames = pendingBytes / bytesPerFrame; - if (pendingFrames > 0) { - long timeUs = wavHeader.getTimeUs(input.getPosition() - pendingBytes); - int size = pendingFrames * bytesPerFrame; - pendingBytes -= size; - trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, null); - } - - return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE; + return outputWriter.sampleData(input, bytesLeft) ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } + @EnsuresNonNull({"extractorOutput", "trackOutput"}) + private void assertInitialized() { + Assertions.checkStateNotNull(trackOutput); + Util.castNonNull(extractorOutput); + } + + /** Writes to the extractor's output. */ + private interface OutputWriter { + + /** + * Resets the writer. + * + * @param timeUs The new start position in microseconds. + */ + void reset(long timeUs); + + /** + * Initializes the writer. + * + *

Must be called once, before any calls to {@link #sampleData(ExtractorInput, long)}. + * + * @param dataStartPosition The byte position (inclusive) in the stream at which data starts. + * @param dataEndPosition The end position (exclusive) in the stream at which data ends. + * @throws ParserException If an error occurs initializing the writer. + */ + void init(int dataStartPosition, long dataEndPosition) throws ParserException; + + /** + * Consumes sample data from {@code input}, writing corresponding samples to the extractor's + * output. + * + *

Must not be called until after {@link #init(int, long)} has been called. + * + * @param input The input from which to read. + * @param bytesLeft The number of sample data bytes left to be read from the input. + * @return Whether the end of the sample data has been reached. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException; + } + + private static final class PassthroughOutputWriter implements OutputWriter { + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + private final Format format; + /** The target size of each output sample, in bytes. */ + private final int targetSampleSizeBytes; + + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public PassthroughOutputWriter( + ExtractorOutput extractorOutput, + TrackOutput trackOutput, + WavHeader header, + String mimeType, + @C.PcmEncoding int pcmEncoding) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + + int bytesPerFrame = header.numChannels * header.bitsPerSample / 8; + // Validate the header. Blocks are expected to correspond to single frames. + if (header.blockSize != bytesPerFrame) { + throw new ParserException( + "Expected block size: " + bytesPerFrame + "; got: " + header.blockSize); + } + + targetSampleSizeBytes = + Math.max(bytesPerFrame, header.frameRateHz * bytesPerFrame / TARGET_SAMPLES_PER_SECOND); + format = + Format.createAudioSampleFormat( + /* id= */ null, + mimeType, + /* codecs= */ null, + /* bitrate= */ header.frameRateHz * bytesPerFrame * 8, + /* maxInputSize= */ targetSampleSizeBytes, + header.numChannels, + header.frameRateHz, + pcmEncoding, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } + + @Override + public void reset(long timeUs) { + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, /* framesPerBlock= */ 1, dataStartPosition, dataEndPosition)); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Write sample data until we've reached the target sample size, or the end of the data. + while (bytesLeft > 0 && pendingOutputBytes < targetSampleSizeBytes) { + int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); + int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); + if (bytesAppended == RESULT_END_OF_INPUT) { + bytesLeft = 0; + } else { + pendingOutputBytes += bytesAppended; + bytesLeft -= bytesAppended; + } + } + + // Write the corresponding sample metadata. Samples must be a whole number of frames. It's + // possible that the number of pending output bytes is not a whole number of frames if the + // stream ended unexpectedly. + int bytesPerFrame = header.blockSize; + int pendingFrames = pendingOutputBytes / bytesPerFrame; + if (pendingFrames > 0) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp( + outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = pendingFrames * bytesPerFrame; + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += pendingFrames; + pendingOutputBytes = offset; + } + + return bytesLeft <= 0; + } + } + + private static final class ImaAdPcmOutputWriter implements OutputWriter { + + private static final int[] INDEX_TABLE = { + -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 + }; + + private static final int[] STEP_TABLE = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, + 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, + 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + private final ExtractorOutput extractorOutput; + private final TrackOutput trackOutput; + private final WavHeader header; + + /** Number of frames per block of the input (yet to be decoded) data. */ + private final int framesPerBlock; + /** Target for the input (yet to be decoded) data. */ + private final byte[] inputData; + /** Target for decoded (yet to be output) data. */ + private final ParsableByteArray decodedData; + /** The target size of each output sample, in frames. */ + private final int targetSampleSizeFrames; + /** The output format. */ + private final Format format; + + /** The number of pending bytes in {@link #inputData}. */ + private int pendingInputBytes; + /** The time at which the writer was last {@link #reset}. */ + private long startTimeUs; + /** + * The number of bytes that have been written to {@link #trackOutput} but have yet to be + * included as part of a sample (i.e. the corresponding call to {@link + * TrackOutput#sampleMetadata} has yet to be made). + */ + private int pendingOutputBytes; + /** + * The total number of frames in samples that have been written to the trackOutput since the + * last call to {@link #reset}. + */ + private long outputFrameCount; + + public ImaAdPcmOutputWriter( + ExtractorOutput extractorOutput, TrackOutput trackOutput, WavHeader header) + throws ParserException { + this.extractorOutput = extractorOutput; + this.trackOutput = trackOutput; + this.header = header; + targetSampleSizeFrames = Math.max(1, header.frameRateHz / TARGET_SAMPLES_PER_SECOND); + + ParsableByteArray scratch = new ParsableByteArray(header.extraData); + scratch.readLittleEndianUnsignedShort(); + framesPerBlock = scratch.readLittleEndianUnsignedShort(); + + int numChannels = header.numChannels; + // Validate the header. This calculation is defined in "Microsoft Multimedia Standards Update + // - New Multimedia Types and Data Techniques" (1994). See the "IMA ADPCM Wave Type" and "DVI + // ADPCM Wave Type" sections, and the calculation of wSamplesPerBlock in the latter. + int expectedFramesPerBlock = + (((header.blockSize - (4 * numChannels)) * 8) / (header.bitsPerSample * numChannels)) + 1; + if (framesPerBlock != expectedFramesPerBlock) { + throw new ParserException( + "Expected frames per block: " + expectedFramesPerBlock + "; got: " + framesPerBlock); + } + + // Calculate the number of blocks we'll need to decode to obtain an output sample of the + // target sample size, and allocate suitably sized buffers for input and decoded data. + int maxBlocksToDecode = Util.ceilDivide(targetSampleSizeFrames, framesPerBlock); + inputData = new byte[maxBlocksToDecode * header.blockSize]; + decodedData = + new ParsableByteArray( + maxBlocksToDecode * numOutputFramesToBytes(framesPerBlock, numChannels)); + + // Create the format. We calculate the bitrate of the data before decoding, since this is the + // bitrate of the stream itself. + int bitrate = header.frameRateHz * header.blockSize * 8 / framesPerBlock; + format = + Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_RAW, + /* codecs= */ null, + bitrate, + /* maxInputSize= */ numOutputFramesToBytes(targetSampleSizeFrames, numChannels), + header.numChannels, + header.frameRateHz, + C.ENCODING_PCM_16BIT, + /* initializationData= */ null, + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null); + } + + @Override + public void reset(long timeUs) { + pendingInputBytes = 0; + startTimeUs = timeUs; + pendingOutputBytes = 0; + outputFrameCount = 0; + } + + @Override + public void init(int dataStartPosition, long dataEndPosition) { + extractorOutput.seekMap( + new WavSeekMap(header, framesPerBlock, dataStartPosition, dataEndPosition)); + trackOutput.format(format); + } + + @Override + public boolean sampleData(ExtractorInput input, long bytesLeft) + throws IOException, InterruptedException { + // Calculate the number of additional frames that we need on the output side to complete a + // sample of the target size. + int targetFramesRemaining = + targetSampleSizeFrames - numOutputBytesToFrames(pendingOutputBytes); + // Calculate the whole number of blocks that we need to decode to obtain this many frames. + int blocksToDecode = Util.ceilDivide(targetFramesRemaining, framesPerBlock); + int targetReadBytes = blocksToDecode * header.blockSize; + + // Read input data until we've reached the target number of blocks, or the end of the data. + boolean endOfSampleData = bytesLeft == 0; + while (!endOfSampleData && pendingInputBytes < targetReadBytes) { + int bytesToRead = (int) Math.min(targetReadBytes - pendingInputBytes, bytesLeft); + int bytesAppended = input.read(inputData, pendingInputBytes, bytesToRead); + if (bytesAppended == RESULT_END_OF_INPUT) { + endOfSampleData = true; + } else { + pendingInputBytes += bytesAppended; + } + } + + int pendingBlockCount = pendingInputBytes / header.blockSize; + if (pendingBlockCount > 0) { + // We have at least one whole block to decode. + decode(inputData, pendingBlockCount, decodedData); + pendingInputBytes -= pendingBlockCount * header.blockSize; + + // Write all of the decoded data to the track output. + int decodedDataSize = decodedData.limit(); + trackOutput.sampleData(decodedData, decodedDataSize); + pendingOutputBytes += decodedDataSize; + + // Output the next sample at the target size. + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames >= targetSampleSizeFrames) { + writeSampleMetadata(targetSampleSizeFrames); + } + } + + // If we've reached the end of the data, we might need to output a final partial sample. + if (endOfSampleData) { + int pendingOutputFrames = numOutputBytesToFrames(pendingOutputBytes); + if (pendingOutputFrames > 0) { + writeSampleMetadata(pendingOutputFrames); + } + } + + return endOfSampleData; + } + + private void writeSampleMetadata(int sampleFrames) { + long timeUs = + startTimeUs + + Util.scaleLargeTimestamp(outputFrameCount, C.MICROS_PER_SECOND, header.frameRateHz); + int size = numOutputFramesToBytes(sampleFrames); + int offset = pendingOutputBytes - size; + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, offset, /* encryptionData= */ null); + outputFrameCount += sampleFrames; + pendingOutputBytes -= size; + } + + /** + * Decodes IMA ADPCM data to 16 bit PCM. + * + * @param input The input data to decode. + * @param blockCount The number of blocks to decode. + * @param output The output into which the decoded data will be written. + */ + private void decode(byte[] input, int blockCount, ParsableByteArray output) { + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) { + for (int channelIndex = 0; channelIndex < header.numChannels; channelIndex++) { + decodeBlockForChannel(input, blockIndex, channelIndex, output.data); + } + } + int decodedDataSize = numOutputFramesToBytes(framesPerBlock * blockCount); + output.reset(decodedDataSize); + } + + private void decodeBlockForChannel( + byte[] input, int blockIndex, int channelIndex, byte[] output) { + int blockSize = header.blockSize; + int numChannels = header.numChannels; + + // The input data consists for a four byte header [Ci] for each of the N channels, followed + // by interleaved data segments [Ci-DATAj], each of which are four bytes long. + // + // [C1][C2]...[CN] [C1-Data0][C2-Data0]...[CN-Data0] [C1-Data1][C2-Data1]...[CN-Data1] etc + // + // Compute the start indices for the [Ci] and [Ci-Data0] for the current channel, as well as + // the number of data bytes for the channel in the block. + int blockStartIndex = blockIndex * blockSize; + int headerStartIndex = blockStartIndex + channelIndex * 4; + int dataStartIndex = headerStartIndex + numChannels * 4; + int dataSizeBytes = blockSize / numChannels - 4; + + // Decode initialization. Casting to a short is necessary for the most significant bit to be + // treated as -2^15 rather than 2^15. + int predictedSample = + (short) (((input[headerStartIndex + 1] & 0xFF) << 8) | (input[headerStartIndex] & 0xFF)); + int stepIndex = Math.min(input[headerStartIndex + 2] & 0xFF, 88); + int step = STEP_TABLE[stepIndex]; + + // Output the initial 16 bit PCM sample from the header. + int outputIndex = (blockIndex * framesPerBlock * numChannels + channelIndex) * 2; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + // We examine each data byte twice during decode. + for (int i = 0; i < dataSizeBytes * 2; i++) { + int dataSegmentIndex = i / 8; + int dataSegmentOffset = (i / 2) % 4; + int dataIndex = dataStartIndex + (dataSegmentIndex * numChannels * 4) + dataSegmentOffset; + + int originalSample = input[dataIndex] & 0xFF; + if (i % 2 == 0) { + originalSample &= 0x0F; // Bottom four bits. + } else { + originalSample >>= 4; // Top four bits. + } + + int delta = originalSample & 0x07; + int difference = ((2 * delta + 1) * step) >> 3; + + if ((originalSample & 0x08) != 0) { + difference = -difference; + } + + predictedSample += difference; + predictedSample = Util.constrainValue(predictedSample, /* min= */ -32768, /* max= */ 32767); + + // Output the next 16 bit PCM sample to the correct position in the output. + outputIndex += 2 * numChannels; + output[outputIndex] = (byte) (predictedSample & 0xFF); + output[outputIndex + 1] = (byte) (predictedSample >> 8); + + stepIndex += INDEX_TABLE[originalSample]; + stepIndex = Util.constrainValue(stepIndex, /* min= */ 0, /* max= */ STEP_TABLE.length - 1); + step = STEP_TABLE[stepIndex]; + } + } + + private int numOutputBytesToFrames(int bytes) { + return bytes / (2 * header.numChannels); + } + + private int numOutputFramesToBytes(int frames) { + return numOutputFramesToBytes(frames, header.numChannels); + } + + private static int numOutputFramesToBytes(int frames, int numChannels) { + return frames * 2 * numChannels; + } + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java index 6e3c5988a..ca34e32cc 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java @@ -15,151 +15,41 @@ */ package com.google.android.exoplayer2.extractor.wav; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.SeekPoint; -import com.google.android.exoplayer2.util.Util; - /** Header for a WAV file. */ -/* package */ final class WavHeader implements SeekMap { +/* 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. */ - @C.PcmEncoding private final int encoding; - - /** Position of the start of the sample data, in bytes. */ - private int dataStartPosition; - /** Position of the end of the sample data (exclusive), in bytes. */ - private long dataEndPosition; + /** + * The format type. Standard format types are the "WAVE form Registration Number" constants + * defined in RFC 2361 Appendix A. + */ + public final int formatType; + /** The number of channels. */ + public final int numChannels; + /** The sample rate in Hertz. */ + public final int frameRateHz; + /** The average bytes per second for the sample data. */ + public final int averageBytesPerSecond; + /** The block size in bytes. */ + public final int blockSize; + /** Bits per sample for a single channel. */ + public final int bitsPerSample; + /** Extra data appended to the format chunk of the header. */ + public final byte[] extraData; public WavHeader( + int formatType, int numChannels, - int sampleRateHz, + int frameRateHz, int averageBytesPerSecond, - int blockAlignment, + int blockSize, int bitsPerSample, - @C.PcmEncoding int encoding) { + byte[] extraData) { + this.formatType = formatType; this.numChannels = numChannels; - this.sampleRateHz = sampleRateHz; + this.frameRateHz = frameRateHz; this.averageBytesPerSecond = averageBytesPerSecond; - this.blockAlignment = blockAlignment; + this.blockSize = blockSize; this.bitsPerSample = bitsPerSample; - this.encoding = encoding; - dataStartPosition = C.POSITION_UNSET; - dataEndPosition = C.POSITION_UNSET; + this.extraData = extraData; } - - // Data bounds. - - /** - * Sets the data start position and size in bytes of sample data in this WAV. - * - * @param dataStartPosition The position of the start of the sample data, in bytes. - * @param dataEndPosition The position of the end of the sample data (exclusive), in bytes. - */ - public void setDataBounds(int dataStartPosition, long dataEndPosition) { - this.dataStartPosition = dataStartPosition; - this.dataEndPosition = dataEndPosition; - } - - /** - * Returns the position of the start of the sample data, in bytes, or {@link C#POSITION_UNSET} if - * the data bounds have not been set. - */ - public int getDataStartPosition() { - return dataStartPosition; - } - - /** - * Returns the position of the end of the sample data (exclusive), in bytes, or {@link - * C#POSITION_UNSET} if the data bounds have not been set. - */ - public long getDataEndPosition() { - return dataEndPosition; - } - - /** Returns whether the data start position and size have been set. */ - public boolean hasDataBounds() { - return dataStartPosition != C.POSITION_UNSET; - } - - // SeekMap implementation. - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getDurationUs() { - long numFrames = (dataEndPosition - dataStartPosition) / blockAlignment; - return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; - } - - @Override - public SeekPoints getSeekPoints(long timeUs) { - long dataSize = dataEndPosition - dataStartPosition; - long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Constrain to nearest preceding frame offset. - positionOffset = (positionOffset / blockAlignment) * blockAlignment; - positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); - long seekPosition = dataStartPosition + positionOffset; - long seekTimeUs = getTimeUs(seekPosition); - SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); - if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlignment) { - return new SeekPoints(seekPoint); - } else { - long secondSeekPosition = seekPosition + blockAlignment; - long secondSeekTimeUs = getTimeUs(secondSeekPosition); - SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); - return new SeekPoints(seekPoint, secondSeekPoint); - } - } - - // Misc getters. - - /** - * Returns the time in microseconds for the given position in bytes. - * - * @param position The position in bytes. - */ - public long getTimeUs(long position) { - long positionOffset = Math.max(0, position - dataStartPosition); - return (positionOffset * C.MICROS_PER_SECOND) / averageBytesPerSecond; - } - - /** 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 PCM encoding. **/ - public @C.PcmEncoding int getEncoding() { - return encoding; - } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index bbcb75aa2..b2cdda7f9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.wav; +import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.audio.WavUtil; @@ -22,6 +24,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ @@ -39,6 +42,7 @@ import java.io.IOException; * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a * supported WAV format. */ + @Nullable public static WavHeader peek(ExtractorInput input) throws IOException, InterruptedException { Assertions.checkNotNull(input); @@ -69,51 +73,46 @@ import java.io.IOException; Assertions.checkState(chunkHeader.size >= 16); input.peekFully(scratch.data, 0, 16); scratch.setPosition(0); - int type = scratch.readLittleEndianUnsignedShort(); + int audioFormatType = scratch.readLittleEndianUnsignedShort(); int numChannels = scratch.readLittleEndianUnsignedShort(); - int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt(); + int frameRateHz = scratch.readLittleEndianUnsignedIntToInt(); int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt(); - int blockAlignment = scratch.readLittleEndianUnsignedShort(); + int blockSize = scratch.readLittleEndianUnsignedShort(); int bitsPerSample = scratch.readLittleEndianUnsignedShort(); - int expectedBlockAlignment = numChannels * bitsPerSample / 8; - if (blockAlignment != expectedBlockAlignment) { - throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: " - + blockAlignment); + int bytesLeft = (int) chunkHeader.size - 16; + byte[] extraData; + if (bytesLeft > 0) { + extraData = new byte[bytesLeft]; + input.peekFully(extraData, 0, bytesLeft); + } else { + extraData = Util.EMPTY_BYTE_ARRAY; } - @C.PcmEncoding int encoding = WavUtil.getEncodingForType(type, bitsPerSample); - if (encoding == C.ENCODING_INVALID) { - Log.e(TAG, "Unsupported WAV format: " + bitsPerSample + " bit/sample, 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); + audioFormatType, + numChannels, + frameRateHz, + averageBytesPerSecond, + blockSize, + bitsPerSample, + extraData); } /** - * Skips to the data in the given WAV input stream. After calling, the input stream's position - * will point to the start of sample data in the WAV, and the data bounds of the provided {@link - * WavHeader} will have been set. + * Skips to the data in the given WAV input stream, and returns its bounds. 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. * - *

If an exception is thrown, the input position will be left pointing to a chunk header and - * the bounds of the provided {@link WavHeader} will not have been set. - * - * @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. + * @param input The input stream, whose read position must be pointing to a valid chunk header. + * @return The byte positions at which the data starts (inclusive) and ends (exclusive). * @throws ParserException If an error occurs parsing chunks. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from input. */ - public static void skipToData(ExtractorInput input, WavHeader wavHeader) + public static Pair skipToData(ExtractorInput input) throws IOException, InterruptedException { 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(); @@ -139,14 +138,14 @@ import java.io.IOException; // Skip past the "data" header. input.skipFully(ChunkHeader.SIZE_IN_BYTES); - int dataStartPosition = (int) input.getPosition(); + long dataStartPosition = input.getPosition(); long dataEndPosition = dataStartPosition + chunkHeader.size; long inputLength = input.getLength(); if (inputLength != C.LENGTH_UNSET && dataEndPosition > inputLength) { Log.w(TAG, "Data exceeds input length: " + dataEndPosition + ", " + inputLength); dataEndPosition = inputLength; } - wavHeader.setDataBounds(dataStartPosition, dataEndPosition); + return Pair.create(dataStartPosition, dataEndPosition); } private WavHeaderReader() { @@ -180,7 +179,7 @@ import java.io.IOException; */ public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch) throws IOException, InterruptedException { - input.peekFully(scratch.data, 0, SIZE_IN_BYTES); + input.peekFully(scratch.data, /* offset= */ 0, /* length= */ SIZE_IN_BYTES); scratch.setPosition(0); int id = scratch.readInt(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java new file mode 100644 index 000000000..2a92c3843 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/extractor/wav/WavSeekMap.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.extractor.wav; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; +import com.google.android.exoplayer2.util.Util; + +/* package */ final class WavSeekMap implements SeekMap { + + private final WavHeader wavHeader; + private final int framesPerBlock; + private final long firstBlockPosition; + private final long blockCount; + private final long durationUs; + + public WavSeekMap( + WavHeader wavHeader, int framesPerBlock, long dataStartPosition, long dataEndPosition) { + this.wavHeader = wavHeader; + this.framesPerBlock = framesPerBlock; + this.firstBlockPosition = dataStartPosition; + this.blockCount = (dataEndPosition - dataStartPosition) / wavHeader.blockSize; + durationUs = blockIndexToTimeUs(blockCount); + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + // Calculate the containing block index, constraining to valid indices. + long blockIndex = (timeUs * wavHeader.frameRateHz) / (C.MICROS_PER_SECOND * framesPerBlock); + blockIndex = Util.constrainValue(blockIndex, 0, blockCount - 1); + + long seekPosition = firstBlockPosition + (blockIndex * wavHeader.blockSize); + long seekTimeUs = blockIndexToTimeUs(blockIndex); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || blockIndex == blockCount - 1) { + return new SeekPoints(seekPoint); + } else { + long secondBlockIndex = blockIndex + 1; + long secondSeekPosition = firstBlockPosition + (secondBlockIndex * wavHeader.blockSize); + long secondSeekTimeUs = blockIndexToTimeUs(secondBlockIndex); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } + } + + private long blockIndexToTimeUs(long blockIndex) { + return Util.scaleLargeTimestamp( + blockIndex * framesPerBlock, C.MICROS_PER_SECOND, wavHeader.frameRateHz); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 8581f279d..60c29f618 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -22,8 +22,8 @@ import android.media.MediaCodecInfo.AudioCapabilities; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -51,7 +51,7 @@ public final class MediaCodecInfo { public final String name; /** The MIME type handled by the codec, or {@code null} if this is a passthrough codec. */ - public final @Nullable String mimeType; + @Nullable public final String mimeType; /** * The MIME type that the codec uses for media of type {@link #mimeType}, or {@code null} if this @@ -64,7 +64,7 @@ public final class MediaCodecInfo { * The capabilities of the decoder, like the profiles/levels it supports, or {@code null} if not * known. */ - public final @Nullable CodecCapabilities capabilities; + @Nullable public final CodecCapabilities capabilities; /** * Whether the decoder supports seamless resolution switches. @@ -93,6 +93,33 @@ public final class MediaCodecInfo { /** Whether this instance describes a passthrough codec. */ public final boolean passthrough; + /** + * Whether the codec is hardware accelerated. + * + *

This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isHardwareAccelerated() + */ + public final boolean hardwareAccelerated; + + /** + * Whether the codec is software only. + * + *

This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isSoftwareOnly() + */ + public final boolean softwareOnly; + + /** + * Whether the codec is from the vendor. + * + *

This could be an approximation as the exact information is only provided in API levels 29+. + * + * @see android.media.MediaCodecInfo#isVendor() + */ + public final boolean vendor; + private final boolean isVideo; /** @@ -108,6 +135,9 @@ public final class MediaCodecInfo { /* codecMimeType= */ null, /* capabilities= */ null, /* passthrough= */ true, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, /* forceDisableAdaptive= */ false, /* forceSecure= */ false); } @@ -121,6 +151,9 @@ public final class MediaCodecInfo { * Equal to {@code mimeType} unless the codec is known to use a non-standard MIME type alias. * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type, or * {@code null} if not known. + * @param hardwareAccelerated Whether the {@link MediaCodec} is hardware accelerated. + * @param softwareOnly Whether the {@link MediaCodec} is software only. + * @param vendor Whether the {@link MediaCodec} is provided by the vendor. * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. * @param forceSecure Whether {@link #secure} should be forced to {@code true}. * @return The created instance. @@ -130,6 +163,9 @@ public final class MediaCodecInfo { String mimeType, String codecMimeType, @Nullable CodecCapabilities capabilities, + boolean hardwareAccelerated, + boolean softwareOnly, + boolean vendor, boolean forceDisableAdaptive, boolean forceSecure) { return new MediaCodecInfo( @@ -138,6 +174,9 @@ public final class MediaCodecInfo { codecMimeType, capabilities, /* passthrough= */ false, + hardwareAccelerated, + softwareOnly, + vendor, forceDisableAdaptive, forceSecure); } @@ -148,6 +187,9 @@ public final class MediaCodecInfo { @Nullable String codecMimeType, @Nullable CodecCapabilities capabilities, boolean passthrough, + boolean hardwareAccelerated, + boolean softwareOnly, + boolean vendor, boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); @@ -155,6 +197,9 @@ public final class MediaCodecInfo { this.codecMimeType = codecMimeType; this.capabilities = capabilities; this.passthrough = passthrough; + this.hardwareAccelerated = hardwareAccelerated; + this.softwareOnly = softwareOnly; + this.vendor = vendor; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); secure = forceSecure || (capabilities != null && isSecure(capabilities)); @@ -197,7 +242,7 @@ public final class MediaCodecInfo { * @throws MediaCodecUtil.DecoderQueryException Thrown if an error occurs while querying decoders. */ public boolean isFormatSupported(Format format) throws MediaCodecUtil.DecoderQueryException { - if (!isCodecSupported(format.codecs)) { + if (!isCodecSupported(format)) { return false; } @@ -225,25 +270,25 @@ public final class MediaCodecInfo { } /** - * Whether the decoder supports the given {@code codec}. If there is insufficient information to - * decide, returns true. + * Whether the decoder supports the codec of the given {@code format}. If there is insufficient + * information to decide, returns true. * - * @param codec Codec string as defined in RFC 6381. - * @return True if the given codec is supported by the decoder. + * @param format The input media format. + * @return True if the codec of the given {@code format} is supported by the decoder. */ - public boolean isCodecSupported(String codec) { - if (codec == null || mimeType == null) { + public boolean isCodecSupported(Format format) { + if (format.codecs == null || mimeType == null) { return true; } - String codecMimeType = MimeTypes.getMediaMimeType(codec); + String codecMimeType = MimeTypes.getMediaMimeType(format.codecs); if (codecMimeType == null) { return true; } if (!mimeType.equals(codecMimeType)) { - logNoSupport("codec.mime " + codec + ", " + codecMimeType); + logNoSupport("codec.mime " + format.codecs + ", " + codecMimeType); return false; } - Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel == null) { // If we don't know any better, we assume that the profile and level are supported. return true; @@ -260,7 +305,19 @@ public final class MediaCodecInfo { return true; } } - logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType); + logNoSupport("codec.profileLevel, " + format.codecs + ", " + codecMimeType); + return false; + } + + /** Whether the codec handles HDR10+ out-of-band metadata. */ + public boolean isHdr10PlusOutOfBandMetadataSupported() { + if (Util.SDK_INT >= 29 && MimeTypes.VIDEO_VP9.equals(mimeType)) { + for (CodecProfileLevel capabilities : getProfileLevels()) { + if (capabilities.profile == CodecProfileLevel.VP9Profile2HDR10Plus) { + return true; + } + } + } return false; } @@ -278,8 +335,7 @@ public final class MediaCodecInfo { if (isVideo) { return adaptive; } else { - Pair codecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + Pair codecProfileLevel = MediaCodecUtil.getCodecProfileAndLevel(format); return codecProfileLevel != null && codecProfileLevel.first == CodecProfileLevel.AACObjectXHE; } } @@ -313,9 +369,9 @@ public final class MediaCodecInfo { } // Check the codec profile levels support adaptation. Pair oldCodecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(oldFormat.codecs); + MediaCodecUtil.getCodecProfileAndLevel(oldFormat); Pair newCodecProfileLevel = - MediaCodecUtil.getCodecProfileAndLevel(newFormat.codecs); + MediaCodecUtil.getCodecProfileAndLevel(newFormat); if (oldCodecProfileLevel == null || newCodecProfileLevel == null) { return false; } @@ -328,13 +384,13 @@ public final class MediaCodecInfo { /** * Whether the decoder supports video with a given width, height and frame rate. - *

- * Must not be called if the device SDK version is less than 21. + * + *

Must not be called if the device SDK version is less than 21. * * @param width Width in pixels. * @param height Height in pixels. - * @param frameRate Optional frame rate in frames per second. Ignored if set to - * {@link Format#NO_VALUE} or any value less than or equal to 0. + * @param frameRate Optional frame rate in frames per second. Ignored if set to {@link + * Format#NO_VALUE} or any value less than or equal to 0. * @return Whether the decoder supports video with the given width, height and frame rate. */ @TargetApi(21) @@ -349,10 +405,8 @@ public final class MediaCodecInfo { return false; } if (!areSizeAndRateSupportedV21(videoCapabilities, width, height, frameRate)) { - // Capabilities are known to be inaccurately reported for vertical resolutions on some devices - // (b/31387661). If the video is vertical and the capabilities indicate support if the width - // and height are swapped, we assume that the vertical resolution is also supported. if (width >= height + || !enableRotatedVerticalResolutionWorkaround(name) || !areSizeAndRateSupportedV21(videoCapabilities, height, width, frameRate)) { logNoSupport("sizeAndRate.support, " + width + "x" + height + "x" + frameRate); return false; @@ -519,7 +573,9 @@ public final class MediaCodecInfo { width = alignedSize.x; height = alignedSize.y; - if (frameRate == Format.NO_VALUE || frameRate <= 0) { + // VideoCapabilities.areSizeAndRateSupported incorrectly returns false if frameRate < 1 on some + // versions of Android, so we only check the size in this case [Internal ref: b/153940404]. + if (frameRate == Format.NO_VALUE || frameRate < 1) { return capabilities.isSizeSupported(width, height); } else { // The signaled frame rate may be slightly higher than the actual frame rate, so we take the @@ -543,4 +599,21 @@ public final class MediaCodecInfo { private static int getMaxSupportedInstancesV23(CodecCapabilities capabilities) { return capabilities.getMaxSupportedInstances(); } + + /** + * Capabilities are known to be inaccurately reported for vertical resolutions on some devices. + * [Internal ref: b/31387661]. When this workaround is enabled, we also check whether the + * capabilities indicate support if the width and height are swapped. If they do, we assume that + * the vertical resolution is also supported. + * + * @param name The name of the codec. + * @return Whether to enable the workaround. + */ + private static final boolean enableRotatedVerticalResolutionWorkaround(String name) { + if ("OMX.MTK.VIDEO.DECODER.HEVC".equals(name) && "mcv5a".equals(Util.DEVICE)) { + // See https://github.com/google/ExoPlayer/issues/6612. + return false; + } + return true; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 7a6ca82c2..e1026ed19 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -23,7 +23,6 @@ import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaFormat; import android.os.Bundle; -import android.os.Looper; import android.os.SystemClock; import androidx.annotation.CheckResult; import androidx.annotation.IntDef; @@ -60,9 +59,7 @@ import java.util.List; */ public abstract class MediaCodecRenderer extends BaseRenderer { - /** - * Thrown when a failure occurs instantiating a decoder. - */ + /** Thrown when a failure occurs instantiating a decoder. */ public static class DecoderInitializationException extends Exception { private static final int CUSTOM_ERROR_CODE_BASE = -50000; @@ -80,21 +77,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { public final boolean secureDecoderRequired; /** - * The name of the decoder that failed to initialize. Null if no suitable decoder was found. + * The {@link MediaCodecInfo} of the decoder that failed to initialize. Null if no suitable + * decoder was found. */ - public final String decoderName; + @Nullable public final MediaCodecInfo codecInfo; - /** - * An optional developer-readable diagnostic information string. May be null. - */ - public final String diagnosticInfo; + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; /** * If the decoder failed to initialize and another decoder being used as a fallback also failed * to initialize, the {@link DecoderInitializationException} for the fallback decoder. Null if * there was no fallback decoder or no suitable decoders were found. */ - public final @Nullable DecoderInitializationException fallbackDecoderInitializationException; + @Nullable public final DecoderInitializationException fallbackDecoderInitializationException; public DecoderInitializationException(Format format, Throwable cause, boolean secureDecoderRequired, int errorCode) { @@ -103,19 +99,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { cause, format.sampleMimeType, secureDecoderRequired, - /* decoderName= */ null, + /* mediaCodecInfo= */ null, buildCustomDiagnosticInfo(errorCode), /* fallbackDecoderInitializationException= */ null); } - public DecoderInitializationException(Format format, Throwable cause, - boolean secureDecoderRequired, String decoderName) { + public DecoderInitializationException( + Format format, + Throwable cause, + boolean secureDecoderRequired, + MediaCodecInfo mediaCodecInfo) { this( - "Decoder init failed: " + decoderName + ", " + format, + "Decoder init failed: " + mediaCodecInfo.name + ", " + format, cause, format.sampleMimeType, secureDecoderRequired, - decoderName, + mediaCodecInfo, Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null, /* fallbackDecoderInitializationException= */ null); } @@ -125,13 +124,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { Throwable cause, String mimeType, boolean secureDecoderRequired, - @Nullable String decoderName, + @Nullable MediaCodecInfo mediaCodecInfo, @Nullable String diagnosticInfo, @Nullable DecoderInitializationException fallbackDecoderInitializationException) { super(message, cause); this.mimeType = mimeType; this.secureDecoderRequired = secureDecoderRequired; - this.decoderName = decoderName; + this.codecInfo = mediaCodecInfo; this.diagnosticInfo = diagnosticInfo; this.fallbackDecoderInitializationException = fallbackDecoderInitializationException; } @@ -144,7 +143,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { getCause(), mimeType, secureDecoderRequired, - decoderName, + codecInfo, diagnosticInfo, fallbackException); } @@ -159,9 +158,34 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static String buildCustomDiagnosticInfo(int errorCode) { String sign = errorCode < 0 ? "neg_" : ""; - return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode); + return "com.google.android.exoplayer2.mediacodec.MediaCodecRenderer_" + + sign + + Math.abs(errorCode); + } + } + + /** Thrown when a failure occurs in the decoder. */ + public static class DecoderException extends Exception { + + /** The {@link MediaCodecInfo} of the decoder that failed. Null if unknown. */ + @Nullable public final MediaCodecInfo codecInfo; + + /** An optional developer-readable diagnostic information string. May be null. */ + @Nullable public final String diagnosticInfo; + + public DecoderException(Throwable cause, @Nullable MediaCodecInfo codecInfo) { + super("Decoder failed: " + (codecInfo == null ? null : codecInfo.name), cause); + this.codecInfo = codecInfo; + 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; + } } /** Indicates no codec operating rate should be set. */ @@ -297,11 +321,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private final float assumedMinimumCodecOperatingRate; private final DecoderInputBuffer buffer; private final DecoderInputBuffer flagsOnlyBuffer; - private final FormatHolder formatHolder; private final TimedValueQueue formatQueue; private final ArrayList decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; + private boolean drmResourcesAcquired; @Nullable private Format inputFormat; private Format outputFormat; @Nullable private DrmSession codecDrmSession; @@ -320,6 +344,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean codecNeedsReconfigureWorkaround; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; + private boolean codecNeedsSosFlushWorkaround; private boolean codecNeedsEosFlushWorkaround; private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; @@ -340,13 +365,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; - private long lastBufferInStreamPresentationTimeUs; + private boolean codecHasOutputMediaFormat; private long largestQueuedPresentationTimeUs; + private long lastBufferInStreamPresentationTimeUs; private boolean inputStreamEnded; private boolean outputStreamEnded; private boolean waitingForKeys; private boolean waitingForFirstSyncSample; private boolean waitingForFirstSampleInFormat; + private boolean skipMediaCodecStopOnRelease; + private boolean pendingOutputEndOfStream; protected DecoderCounters decoderCounters; @@ -383,7 +411,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { this.assumedMinimumCodecOperatingRate = assumedMinimumCodecOperatingRate; buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); - formatHolder = new FormatHolder(); formatQueue = new TimedValueQueue<>(); decodeOnlyPresentationTimestamps = new ArrayList<>(); outputBufferInfo = new MediaCodec.BufferInfo(); @@ -409,39 +436,59 @@ public abstract class MediaCodecRenderer extends BaseRenderer { this.renderTimeLimitMs = renderTimeLimitMs; } + /** + * Skip calling {@link MediaCodec#stop()} when the underlying MediaCodec is going to be released. + * + *

By default, when the MediaCodecRenderer is releasing the underlying {@link MediaCodec}, it + * first calls {@link MediaCodec#stop()} and then calls {@link MediaCodec#release()}. If this + * feature is enabled, the MediaCodecRenderer will skip the call to {@link MediaCodec#stop()}. + * + *

This method is experimental, and will be renamed or removed in a future release. It should + * only be called before the renderer is used. + * + * @param enabled enable or disable the feature. + */ + public void experimental_setSkipMediaCodecStopOnRelease(boolean enabled) { + skipMediaCodecStopOnRelease = enabled; + } + @Override + @AdaptiveSupport public final int supportsMixedMimeTypeAdaptation() { return ADAPTIVE_NOT_SEAMLESS; } @Override + @Capabilities public final int supportsFormat(Format format) throws ExoPlaybackException { try { return supportsFormat(mediaCodecSelector, drmSessionManager, format); } catch (DecoderQueryException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, format); } } /** - * Returns the extent to which the renderer is capable of supporting a given format. + * Returns the {@link Capabilities} for the given {@link Format}. * * @param mediaCodecSelector The decoder selector. * @param drmSessionManager The renderer's {@link DrmSessionManager}. - * @param format The format. - * @return The extent to which the renderer is capable of supporting the given format. See - * {@link #supportsFormat(Format)} for more detail. + * @param format The {@link Format}. + * @return The {@link Capabilities} for this {@link Format}. * @throws DecoderQueryException If there was an error querying decoders. */ - protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, Format format) + @Capabilities + protected abstract int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException; /** * Returns a list of decoders that can decode media in the specified format, in priority order. * * @param mediaCodecSelector The decoder selector. - * @param format The format for which a decoder is required. + * @param format The {@link Format} for which a decoder is required. * @param requiresSecureDecoder Whether a secure decoder is required. * @return A list of {@link MediaCodecInfo}s corresponding to decoders. May be empty. * @throws DecoderQueryException Thrown if there was an error querying decoders. @@ -455,7 +502,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param codecInfo Information about the {@link MediaCodec} being configured. * @param codec The {@link MediaCodec} to configure. - * @param format The format for which the codec is being configured. + * @param format The {@link Format} for which the codec is being configured. * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if * no codec operating rate should be set. @@ -464,7 +511,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto, + @Nullable MediaCrypto crypto, float codecOperatingRate); protected final void maybeInitCodec() throws ExoPlaybackException { @@ -492,17 +539,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); } catch (MediaCryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } mediaCryptoRequiresSecureDecoder = !sessionMediaCrypto.forceAllowInsecureDecoderComponents && mediaCrypto.requiresSecureDecoderComponent(mimeType); } } - if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) { + if (FrameworkMediaCrypto.WORKAROUND_DEVICE_NEEDS_KEYS_TO_CONFIGURE_CODEC) { @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { // Wait for keys. return; @@ -513,7 +560,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder); } catch (DecoderInitializationException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } } @@ -553,6 +600,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { + if (drmSessionManager != null && !drmResourcesAcquired) { + drmResourcesAcquired = true; + drmSessionManager.prepare(); + } decoderCounters = new DecoderCounters(); } @@ -560,6 +611,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; + pendingOutputEndOfStream = false; flushOrReinitializeCodec(); formatQueue.clear(); } @@ -592,12 +644,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } finally { setSourceDrmSession(null); } + if (drmSessionManager != null && drmResourcesAcquired) { + drmResourcesAcquired = false; + drmSessionManager.release(); + } } protected void releaseCodec() { availableCodecInfos = null; codecInfo = null; codecFormat = null; + codecHasOutputMediaFormat = false; resetInputBuffer(); resetOutputBuffer(); resetCodecBuffers(); @@ -610,7 +667,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codec != null) { decoderCounters.decoderReleaseCount++; try { - codec.stop(); + if (!skipMediaCodecStopOnRelease) { + codec.stop(); + } } finally { codec.release(); } @@ -641,31 +700,42 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - if (outputStreamEnded) { - renderToEndOfStream(); - return; + if (pendingOutputEndOfStream) { + pendingOutputEndOfStream = false; + processEndOfStream(); } - if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) { + try { + if (outputStreamEnded) { + renderToEndOfStream(); + return; + } + if (inputFormat == null && !readToFlagsOnlyBuffer(/* requireFormat= */ true)) { // We still don't have a format and can't make progress without one. return; + } + // We have a format. + maybeInitCodec(); + if (codec != null) { + long drainStartTimeMs = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} + TraceUtil.endSection(); + } else { + decoderCounters.skippedInputBufferCount += skipSource(positionUs); + // We need to read any format changes despite not having a codec so that drmSession can be + // updated, and so that we have the most recent format should the codec be initialized. We + // may also reach the end of the stream. Note that readSource will not read a sample into a + // flags-only buffer. + readToFlagsOnlyBuffer(/* requireFormat= */ false); + } + decoderCounters.ensureUpdated(); + } catch (IllegalStateException e) { + if (isMediaCodecException(e)) { + throw createRendererException(e, inputFormat); + } + throw e; } - // We have a format. - maybeInitCodec(); - if (codec != null) { - long drainStartTimeMs = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer() && shouldContinueFeeding(drainStartTimeMs)) {} - TraceUtil.endSection(); - } else { - decoderCounters.skippedInputBufferCount += skipSource(positionUs); - // We need to read any format changes despite not having a codec so that drmSession can be - // updated, and so that we have the most recent format should the codec be initialized. We may - // also reach the end of the stream. Note that readSource will not read a sample into a - // flags-only buffer. - readToFlagsOnlyBuffer(/* requireFormat= */ false); - } - decoderCounters.ensureUpdated(); } /** @@ -698,6 +768,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (codecDrainAction == DRAIN_ACTION_REINITIALIZE || codecNeedsFlushWorkaround + || (codecNeedsSosFlushWorkaround && !codecHasOutputMediaFormat) || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { releaseCodec(); return true; @@ -729,12 +800,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } - /** Reads into {@link #flagsOnlyBuffer} and returns whether a format was read. */ + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new DecoderException(cause, codecInfo); + } + + /** Reads into {@link #flagsOnlyBuffer} and returns whether a {@link Format} was read. */ private boolean readToFlagsOnlyBuffer(boolean requireFormat) throws ExoPlaybackException { + FormatHolder formatHolder = getFormatHolder(); flagsOnlyBuffer.clear(); int result = readSource(formatHolder, flagsOnlyBuffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } else if (result == C.RESULT_BUFFER_READ && flagsOnlyBuffer.isEndOfStream()) { inputStreamEnded = true; @@ -789,7 +866,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { availableCodecInfos.removeFirst(); DecoderInitializationException exception = new DecoderInitializationException( - inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name); + inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo); if (preferredDecoderInitializationException == null) { preferredDecoderInitializationException = exception; } else { @@ -871,6 +948,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, codecFormat); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); + codecNeedsSosFlushWorkaround = codecNeedsSosFlushWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = @@ -888,6 +966,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; codecReceivedEos = false; codecReceivedBuffers = false; + largestQueuedPresentationTimeUs = C.TIME_UNSET; + lastBufferInStreamPresentationTimeUs = C.TIME_UNSET; codecDrainState = DRAIN_STATE_NONE; codecDrainAction = DRAIN_ACTION_NONE; codecNeedsAdaptationWorkaroundBuffer = false; @@ -951,21 +1031,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private void setSourceDrmSession(@Nullable DrmSession session) { - DrmSession previous = sourceDrmSession; + DrmSession.replaceSession(sourceDrmSession, session); sourceDrmSession = session; - releaseDrmSessionIfUnused(previous); } private void setCodecDrmSession(@Nullable DrmSession session) { - DrmSession previous = codecDrmSession; + DrmSession.replaceSession(codecDrmSession, session); codecDrmSession = session; - releaseDrmSessionIfUnused(previous); - } - - private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { - if (session != null && session != sourceDrmSession && session != codecDrmSession) { - drmSessionManager.releaseSession(session); - } } /** @@ -1010,6 +1082,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } int result; + FormatHolder formatHolder = getFormatHolder(); int adaptiveReconfigurationBytes = 0; if (waitingForKeys) { // We've already read an encrypted sample into buffer, and are waiting for keys. @@ -1043,7 +1116,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { buffer.clear(); codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; } - onInputFormatChanged(formatHolder.format); + onInputFormatChanged(formatHolder); return true; } @@ -1070,7 +1143,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { resetInputBuffer(); } } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return false; } @@ -1109,6 +1182,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { Math.max(largestQueuedPresentationTimeUs, presentationTimeUs); buffer.flip(); + if (buffer.hasSupplementalData()) { + handleInputBufferSupplementalData(buffer); + } onQueueInputBuffer(buffer); if (bufferEncrypted) { @@ -1123,18 +1199,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReconfigurationState = RECONFIGURATION_STATE_NONE; decoderCounters.inputBufferCount++; } catch (CryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } return true; } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + if (codecDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || codecDrmSession.playClearSamplesWithoutKeys()))) { return false; } @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); + throw createRendererException(codecDrmSession.getError(), inputFormat); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -1155,36 +1233,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Called when a new format is read from the upstream {@link MediaPeriod}. + * Called when a new {@link Format} is read from the upstream {@link MediaPeriod}. * - * @param newFormat The new format. + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}. */ - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - Format oldFormat = inputFormat; - inputFormat = newFormat; + @SuppressWarnings("unchecked") + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { waitingForFirstSampleInFormat = true; - - boolean drmInitDataChanged = - !Util.areEqual(newFormat.drmInitData, oldFormat == null ? null : oldFormat.drmInitData); - if (drmInitDataChanged) { - if (newFormat.drmInitData != null) { - if (drmSessionManager == null) { - throw ExoPlaybackException.createForRenderer( - new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); - } - DrmSession session = - drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (session == sourceDrmSession || session == codecDrmSession) { - // We already had this session. The manager must be reference counting, so release it once - // to get the count attributed to this renderer back down to 1. - drmSessionManager.releaseSession(session); - } - setSourceDrmSession(session); - } else { - setSourceDrmSession(null); - } + Format newFormat = Assertions.checkNotNull(formatHolder.format); + if (formatHolder.includesDrmSession) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + sourceDrmSession = + getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); } + inputFormat = newFormat; if (codec == null) { maybeInitCodec(); @@ -1196,7 +1260,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if ((sourceDrmSession == null && codecDrmSession != null) || (sourceDrmSession != null && codecDrmSession == null) - || (sourceDrmSession != null && !codecInfo.secure) + || (sourceDrmSession != codecDrmSession + && !codecInfo.secure + && maybeRequiresSecureDecoder(sourceDrmSession, newFormat)) || (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) { // We might need to switch between the clear and protected output paths, or we're using DRM // prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM @@ -1249,23 +1315,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Called when the output format of the {@link MediaCodec} changes. - *

- * The default implementation is a no-op. + * Called when the output {@link MediaFormat} 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 Thrown if an error occurs handling the new output format. + * @param outputMediaFormat The new output {@link MediaFormat}. + * @throws ExoPlaybackException Thrown if an error occurs handling the new output media format. */ - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) + throws ExoPlaybackException { + // Do nothing. + } + + /** + * Handles supplemental data associated with an input buffer. + * + *

The default implementation is a no-op. + * + * @param buffer The input buffer that is about to be queued. + * @throws ExoPlaybackException Thrown if an error occurs handling supplemental data. + */ + protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) throws ExoPlaybackException { // Do nothing. } /** * Called immediately before an input buffer is queued into the codec. - *

- * The default implementation is a no-op. + * + *

The default implementation is a no-op. * * @param buffer The buffer to be queued. */ @@ -1285,15 +1364,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Determines whether the existing {@link MediaCodec} can be kept for a new format, and if it can - * whether it requires reconfiguration. + * Determines whether the existing {@link MediaCodec} can be kept for a new {@link Format}, and if + * it can whether it requires reconfiguration. * *

The default implementation returns {@link #KEEP_CODEC_RESULT_NO}. * * @param codec The existing {@link MediaCodec} instance. * @param codecInfo A {@link MediaCodecInfo} describing the decoder. - * @param oldFormat The format for which the existing instance is configured. - * @param newFormat The new format. + * @param oldFormat The {@link Format} for which the existing instance is configured. + * @param newFormat The new {@link Format}. * @return Whether the instance can be kept, and if it can whether it requires reconfiguration. */ protected @KeepCodecResult int canKeepCodec( @@ -1327,12 +1406,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * Returns the {@link MediaFormat#KEY_OPERATING_RATE} value for a given renderer operating rate, - * current format and set of possible stream formats. + * current {@link Format} and set of possible stream formats. * *

The default implementation returns {@link #CODEC_OPERATING_RATE_UNSET}. * * @param operatingRate The renderer operating rate. - * @param format The format for which the codec is being configured. + * @param format The {@link Format} for which the codec is being configured. * @param streamFormats The possible stream formats. * @return The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if no codec operating * rate should be set. @@ -1534,22 +1613,22 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } - /** - * Processes a new output format. - */ + /** Processes a new output {@link MediaFormat}. */ private void processOutputFormat() throws ExoPlaybackException { - MediaFormat format = codec.getOutputFormat(); + codecHasOutputMediaFormat = true; + MediaFormat mediaFormat = codec.getOutputFormat(); if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER - && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT - && format.getInteger(MediaFormat.KEY_HEIGHT) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) { + && mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT + && mediaFormat.getInteger(MediaFormat.KEY_HEIGHT) + == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) { // We assume this format changed event was caused by the adaptation workaround. shouldSkipAdaptationWorkaroundOutputBuffer = true; return; } if (codecNeedsMonoChannelCountWorkaround) { - format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); } - onOutputFormatChanged(codec, format); + onOutputFormatChanged(codec, mediaFormat); } /** @@ -1587,7 +1666,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param isDecodeOnlyBuffer Whether the buffer was marked with {@link C#BUFFER_FLAG_DECODE_ONLY} * by the source. * @param isLastBuffer Whether the buffer is the last sample of the current stream. - * @param format The format associated with the buffer. + * @param format The {@link Format} associated with the buffer. * @return Whether the output buffer was fully processed (e.g. rendered or skipped). * @throws ExoPlaybackException If an error occurs processing the output buffer. */ @@ -1639,14 +1718,35 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + /** + * Notifies the renderer that output end of stream is pending and should be handled on the next + * render. + */ + protected final void setPendingOutputEndOfStream() { + pendingOutputEndOfStream = true; + } + private void reinitializeCodec() throws ExoPlaybackException { releaseCodec(); maybeInitCodec(); } + private boolean isDecodeOnlyBuffer(long presentationTimeUs) { + // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would + // box presentationTimeUs, creating a Long object that would need to be garbage collected. + int size = decodeOnlyPresentationTimestamps.size(); + for (int i = 0; i < size; i++) { + if (decodeOnlyPresentationTimestamps.get(i) == presentationTimeUs) { + decodeOnlyPresentationTimestamps.remove(i); + return true; + } + } + return false; + } + @TargetApi(23) private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException { - FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto(); + @Nullable FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto(); if (sessionMediaCrypto == null) { // We'd only expect this to happen if the CDM from which the pending session is obtained needs // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme @@ -1673,24 +1773,45 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId); } catch (MediaCryptoException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + throw createRendererException(e, inputFormat); } setCodecDrmSession(sourceDrmSession); codecDrainState = DRAIN_STATE_NONE; codecDrainAction = DRAIN_ACTION_NONE; } - private boolean isDecodeOnlyBuffer(long presentationTimeUs) { - // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would - // box presentationTimeUs, creating a Long object that would need to be garbage collected. - int size = decodeOnlyPresentationTimestamps.size(); - for (int i = 0; i < size; i++) { - if (decodeOnlyPresentationTimestamps.get(i) == presentationTimeUs) { - decodeOnlyPresentationTimestamps.remove(i); - return true; - } + /** + * Returns whether a {@link DrmSession} may require a secure decoder for a given {@link Format}. + * + * @param drmSession The {@link DrmSession}. + * @param format The {@link Format}. + * @return Whether a secure decoder may be required. + */ + private static boolean maybeRequiresSecureDecoder( + DrmSession drmSession, Format format) { + @Nullable FrameworkMediaCrypto sessionMediaCrypto = drmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + // We'd only expect this to happen if the CDM from which the pending session is obtained needs + // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme + // to another, where the new CDM hasn't been used before and needs provisioning). Assume that + // a secure decoder may be required. + return true; + } + if (sessionMediaCrypto.forceAllowInsecureDecoderComponents) { + return false; + } + MediaCrypto mediaCrypto; + try { + mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + // This shouldn't happen, but if it does then assume that a secure decoder may be required. + return true; + } + try { + return mediaCrypto.requiresSecureDecoderComponent(format.sampleMimeType); + } finally { + mediaCrypto.release(); } - return false; } private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( @@ -1709,14 +1830,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return cryptoInfo; } - /** - * Returns whether the device needs keys to have been loaded into the {@link DrmSession} before - * codec configuration. - */ - private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() { - return "Amazon".equals(Util.MANUFACTURER) - && ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1 - || "AFTB".equals(Util.MODEL)); // Fire TV Gen 1 + private static boolean isMediaCodecException(IllegalStateException error) { + if (Util.SDK_INT >= 21 && isMediaCodecExceptionV21(error)) { + return true; + } + StackTraceElement[] stackTrace = error.getStackTrace(); + return stackTrace.length > 0 && stackTrace[0].getClassName().equals("android.media.MediaCodec"); + } + + @TargetApi(21) + private static boolean isMediaCodecExceptionV21(IllegalStateException error) { + return error instanceof MediaCodec.CodecException; } /** @@ -1784,11 +1908,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** * 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. + * + *

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. + * @param format The {@link 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, Format format) { @@ -1852,21 +1977,38 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * 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. + * Returns whether the decoder is known to set the number of audio channels in the output {@link + * Format} to 2 for the given input {@link Format}, whilst only actually outputting a single + * channel. + * + *

If true is returned then we explicitly override the number of channels in the output {@link + * Format}, setting it to 1. * * @param name The decoder name. - * @param format The input format. - * @return True if 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. False - * otherwise. + * @param format The input {@link Format}. + * @return True if the decoder is known to set the number of audio channels in the output {@link + * Format} to 2 for the given input {@link Format}, whilst only actually outputting a single + * channel. False otherwise. */ private static boolean codecNeedsMonoChannelCountWorkaround(String name, Format format) { return Util.SDK_INT <= 18 && format.channelCount == 1 && "OMX.MTK.AUDIO.DECODER.MP3".equals(name); } + /** + * Returns whether the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. + * + *

If true is returned, the renderer will work around the issue by instantiating a new decoder + * when this case occurs. + * + *

See [Internal: b/141097367]. + * + * @param name The name of the decoder. + * @return True if the decoder is known to behave incorrectly if flushed prior to having output a + * {@link MediaFormat}. False otherwise. + */ + private static boolean codecNeedsSosFlushWorkaround(String name) { + return Util.SDK_INT == 29 && "c2.android.aac.decoder".equals(name); + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java index c6e93d104..10ff81147 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java @@ -40,7 +40,8 @@ public interface MediaCodecSelector { } @Override - public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + @Nullable + public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { return MediaCodecUtil.getPassthroughDecoderInfo(); } }; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 671523b5e..2b8dcc0a5 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -20,21 +20,26 @@ import android.annotation.TargetApi; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseIntArray; +import androidx.annotation.CheckResult; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.ColorInfo; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * A utility class for querying the available codecs. @@ -67,6 +72,10 @@ public final class MediaCodecUtil { private static final SparseIntArray AVC_LEVEL_NUMBER_TO_CONST; private static final String CODEC_ID_AVC1 = "avc1"; private static final String CODEC_ID_AVC2 = "avc2"; + // VP9 + private static final SparseIntArray VP9_PROFILE_NUMBER_TO_CONST; + private static final SparseIntArray VP9_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_VP09 = "vp09"; // HEVC. private static final Map HEVC_CODEC_STRING_TO_PROFILE_LEVEL; private static final String CODEC_ID_HEV1 = "hev1"; @@ -74,8 +83,9 @@ public final class MediaCodecUtil { // Dolby Vision. private static final Map DOLBY_VISION_STRING_TO_PROFILE; private static final Map DOLBY_VISION_STRING_TO_LEVEL; - private static final String CODEC_ID_DVHE = "dvhe"; - private static final String CODEC_ID_DVH1 = "dvh1"; + // AV1. + private static final SparseIntArray AV1_LEVEL_NUMBER_TO_CONST; + private static final String CODEC_ID_AV01 = "av01"; // MP4A AAC. private static final SparseIntArray MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE; private static final String CODEC_ID_MP4A = "mp4a"; @@ -114,6 +124,7 @@ public final class MediaCodecUtil { */ @Nullable public static MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException { + @Nullable MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.AUDIO_RAW, /* secure= */ false, /* tunneling= */ false); return decoderInfo == null ? null : MediaCodecInfo.newPassthroughInstance(decoderInfo.name); @@ -153,7 +164,7 @@ public final class MediaCodecUtil { public static synchronized List getDecoderInfos( String mimeType, boolean secure, boolean tunneling) throws DecoderQueryException { CodecKey key = new CodecKey(mimeType, secure, tunneling); - List cachedDecoderInfos = decoderInfosCache.get(key); + @Nullable List cachedDecoderInfos = decoderInfosCache.get(key); if (cachedDecoderInfos != null) { return cachedDecoderInfos; } @@ -178,6 +189,26 @@ public final class MediaCodecUtil { return unmodifiableDecoderInfos; } + /** + * Returns a copy of the provided decoder list sorted such that decoders with format support are + * listed first. The returned list is modifiable for convenience. + */ + @CheckResult + public static List getDecoderInfosSortedByFormatSupport( + List decoderInfos, Format format) { + decoderInfos = new ArrayList<>(decoderInfos); + sortByScore( + decoderInfos, + decoderInfo -> { + try { + return decoderInfo.isFormatSupported(format) ? 1 : 0; + } catch (DecoderQueryException e) { + return -1; + } + }); + return decoderInfos; + } + /** * Returns the maximum frame size supported by the default H264 decoder. * @@ -186,6 +217,7 @@ public final class MediaCodecUtil { public static int maxH264DecodableFrameSize() throws DecoderQueryException { if (maxH264DecodableFrameSize == -1) { int result = 0; + @Nullable MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, /* secure= */ false, /* tunneling= */ false); if (decoderInfo != null) { @@ -202,33 +234,36 @@ public final class MediaCodecUtil { } /** - * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given - * codec description string (as defined by RFC 6381). + * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the codec + * description string (as defined by RFC 6381) of the given format. * - * @param codec A codec description string, as defined by RFC 6381, or {@code null} if not known. - * @return A pair (profile constant, level constant) if {@code codec} is well-formed and - * recognized, or null otherwise + * @param format Media format with a codec description string, as defined by RFC 6381. + * @return A pair (profile constant, level constant) if the codec of the {@code format} is + * well-formed and recognized, or null otherwise. */ @Nullable - public static Pair getCodecProfileAndLevel(@Nullable String codec) { - if (codec == null) { + public static Pair getCodecProfileAndLevel(Format format) { + if (format.codecs == null) { return null; } - // TODO: Check codec profile/level for AV1 once targeting Android Q and [Internal: b/128552878] - // has been fixed. - String[] parts = codec.split("\\."); + String[] parts = format.codecs.split("\\."); + // Dolby Vision can use DV, AVC or HEVC codec IDs, so check the MIME type first. + if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + return getDolbyVisionProfileAndLevel(format.codecs, parts); + } switch (parts[0]) { case CODEC_ID_AVC1: case CODEC_ID_AVC2: - return getAvcProfileAndLevel(codec, parts); + return getAvcProfileAndLevel(format.codecs, parts); + case CODEC_ID_VP09: + return getVp9ProfileAndLevel(format.codecs, parts); case CODEC_ID_HEV1: case CODEC_ID_HVC1: - return getHevcProfileAndLevel(codec, parts); - case CODEC_ID_DVHE: - case CODEC_ID_DVH1: - return getDolbyVisionProfileAndLevel(codec, parts); + return getHevcProfileAndLevel(format.codecs, parts); + case CODEC_ID_AV01: + return getAv1ProfileAndLevel(format.codecs, parts, format.colorInfo); case CODEC_ID_MP4A: - return getAacCodecProfileAndLevel(codec, parts); + return getAacCodecProfileAndLevel(format.codecs, parts); default: return null; } @@ -237,7 +272,7 @@ public final class MediaCodecUtil { // Internal methods. /** - * Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by + * Returns {@link MediaCodecInfo}s for the given codec {@link CodecKey} in the order given by * {@code mediaCodecList}. * * @param key The codec key. @@ -245,8 +280,8 @@ public final class MediaCodecUtil { * @return The codec information for usable codecs matching the specified key. * @throws DecoderQueryException If there was an error querying the available decoders. */ - private static ArrayList getDecoderInfosInternal(CodecKey key, - MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + private static ArrayList getDecoderInfosInternal( + CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { try { ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; @@ -255,8 +290,16 @@ public final class MediaCodecUtil { // Note: MediaCodecList is sorted by the framework such that the best decoders come first. for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); + if (isAlias(codecInfo)) { + // Skip aliases of other codecs, since they will also be listed under their canonical + // names. + continue; + } String name = codecInfo.getName(); - String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); + if (!isCodecUsableDecoder(codecInfo, name, secureDecodersExplicit, mimeType)) { + continue; + } + @Nullable String codecMimeType = getCodecMimeType(codecInfo, name, mimeType); if (codecMimeType == null) { continue; } @@ -280,6 +323,9 @@ public final class MediaCodecUtil { if ((!key.secure && secureRequired) || (key.secure && !secureSupported)) { continue; } + boolean hardwareAccelerated = isHardwareAccelerated(codecInfo); + boolean softwareOnly = isSoftwareOnly(codecInfo); + boolean vendor = isVendor(codecInfo); boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(name); if ((secureDecodersExplicit && key.secure == secureSupported) || (!secureDecodersExplicit && !key.secure)) { @@ -289,6 +335,9 @@ public final class MediaCodecUtil { mimeType, codecMimeType, capabilities, + hardwareAccelerated, + softwareOnly, + vendor, forceDisableAdaptive, /* forceSecure= */ false)); } else if (!secureDecodersExplicit && secureSupported) { @@ -298,6 +347,9 @@ public final class MediaCodecUtil { mimeType, codecMimeType, capabilities, + hardwareAccelerated, + softwareOnly, + vendor, forceDisableAdaptive, /* forceSecure= */ true)); // It only makes sense to have one synthesized secure decoder, return immediately. @@ -329,7 +381,6 @@ public final class MediaCodecUtil { * * @param info The codec information. * @param name The name of the codec - * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. * @param mimeType The MIME type. * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} @@ -339,12 +390,7 @@ public final class MediaCodecUtil { private static String getCodecMimeType( android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit, String mimeType) { - if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { - return null; - } - String[] supportedTypes = info.getSupportedTypes(); for (String supportedType : supportedTypes) { if (supportedType.equalsIgnoreCase(mimeType)) { @@ -391,11 +437,11 @@ public final class MediaCodecUtil { // Work around broken audio decoders. if (Util.SDK_INT < 21 && ("CIPAACDecoder".equals(name) - || "CIPMP3Decoder".equals(name) - || "CIPVorbisDecoder".equals(name) - || "CIPAMRNBDecoder".equals(name) - || "AACDecoder".equals(name) - || "MP3Decoder".equals(name))) { + || "CIPMP3Decoder".equals(name) + || "CIPVorbisDecoder".equals(name) + || "CIPAMRNBDecoder".equals(name) + || "AACDecoder".equals(name) + || "MP3Decoder".equals(name))) { return false; } @@ -404,7 +450,7 @@ public final class MediaCodecUtil { if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) && ("a70".equals(Util.DEVICE) - || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { + || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { return false; } @@ -413,17 +459,17 @@ public final class MediaCodecUtil { 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 - || "ville".equals(Util.DEVICE) // HTC One S - || "villeplus".equals(Util.DEVICE) - || "villec2".equals(Util.DEVICE) - || Util.DEVICE.startsWith("gee") // LGE Optimus G - || "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))) { + || "protou".equals(Util.DEVICE) // HTC Desire X + || "ville".equals(Util.DEVICE) // HTC One S + || "villeplus".equals(Util.DEVICE) + || "villec2".equals(Util.DEVICE) + || Util.DEVICE.startsWith("gee") // LGE Optimus G + || "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; } @@ -431,9 +477,9 @@ public final class MediaCodecUtil { 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))) { + || "C1505".equals(Util.DEVICE) + || "C1604".equals(Util.DEVICE) // Sony Xperia E dual + || "C1605".equals(Util.DEVICE))) { return false; } @@ -442,13 +488,13 @@ public final class MediaCodecUtil { && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 - || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge - || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ - || "SC-05G".equals(Util.DEVICE) // Galaxy S6 - || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active - || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge - || "SC-04G".equals(Util.DEVICE) - || "SCV31".equals(Util.DEVICE))) { + || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge + || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ + || "SC-05G".equals(Util.DEVICE) // Galaxy S6 + || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active + || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge + || "SC-04G".equals(Util.DEVICE) + || "SCV31".equals(Util.DEVICE))) { return false; } @@ -458,10 +504,10 @@ public final class MediaCodecUtil { && "OMX.SEC.vp8.dec".equals(name) && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("d2") - || Util.DEVICE.startsWith("serrano") - || Util.DEVICE.startsWith("jflte") - || Util.DEVICE.startsWith("santos") - || Util.DEVICE.startsWith("t0"))) { + || Util.DEVICE.startsWith("serrano") + || Util.DEVICE.startsWith("jflte") + || Util.DEVICE.startsWith("santos") + || Util.DEVICE.startsWith("t0"))) { return false; } @@ -472,8 +518,7 @@ public final class MediaCodecUtil { } // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041]. - if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) - && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType) && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { return false; } @@ -489,8 +534,43 @@ public final class MediaCodecUtil { */ private static void applyWorkarounds(String mimeType, List decoderInfos) { if (MimeTypes.AUDIO_RAW.equals(mimeType)) { - Collections.sort(decoderInfos, new RawAudioCodecComparator()); - } else if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { + if (Util.SDK_INT < 26 + && Util.DEVICE.equals("R9") + && decoderInfos.size() == 1 + && decoderInfos.get(0).name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This device does not list a generic raw audio decoder, yet it can be instantiated by + // name. See Issue #5782. + decoderInfos.add( + MediaCodecInfo.newInstance( + /* name= */ "OMX.google.raw.decoder", + /* mimeType= */ MimeTypes.AUDIO_RAW, + /* codecMimeType= */ MimeTypes.AUDIO_RAW, + /* capabilities= */ null, + /* hardwareAccelerated= */ false, + /* softwareOnly= */ true, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + } + // Work around inconsistent raw audio decoding behavior across different devices. + sortByScore( + decoderInfos, + decoderInfo -> { + String name = decoderInfo.name; + if (name.startsWith("OMX.google") || name.startsWith("c2.android")) { + // Prefer generic decoders over ones provided by the device. + return 1; + } + if (Util.SDK_INT < 26 && name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { + // This decoder may modify the audio, so any other compatible decoders take + // precedence. See [Internal: b/62337687]. + return -1; + } + return 0; + }); + } + + if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { String firstCodecName = decoderInfos.get(0).name; if ("OMX.SEC.mp3.dec".equals(firstCodecName) || "OMX.SEC.MP3.Decoder".equals(firstCodecName) @@ -499,9 +579,90 @@ public final class MediaCodecUtil { // OMX.brcm.audio.mp3.decoder on older devices. See: // https://github.com/google/ExoPlayer/issues/398 and // https://github.com/google/ExoPlayer/issues/4519. - Collections.sort(decoderInfos, new PreferOmxGoogleCodecComparator()); + sortByScore(decoderInfos, decoderInfo -> decoderInfo.name.startsWith("OMX.google") ? 1 : 0); } } + + if (Util.SDK_INT < 30 && decoderInfos.size() > 1) { + String firstCodecName = decoderInfos.get(0).name; + // Prefer anything other than OMX.qti.audio.decoder.flac on older devices. See [Internal + // ref: b/147278539] and [Internal ref: b/147354613]. + if ("OMX.qti.audio.decoder.flac".equals(firstCodecName)) { + decoderInfos.add(decoderInfos.remove(0)); + } + } + } + + private static boolean isAlias(android.media.MediaCodecInfo info) { + return Util.SDK_INT >= 29 && isAliasV29(info); + } + + @RequiresApi(29) + private static boolean isAliasV29(android.media.MediaCodecInfo info) { + return info.isAlias(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+, + * or a best-effort approximation for lower levels. + */ + private static boolean isHardwareAccelerated(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isHardwareAcceleratedV29(codecInfo); + } + // codecInfo.isHardwareAccelerated() != codecInfo.isSoftwareOnly() is not necessarily true. + // However, we assume this to be true as an approximation. + return !isSoftwareOnly(codecInfo); + } + + @TargetApi(29) + private static boolean isHardwareAcceleratedV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isHardwareAccelerated(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isSoftwareOnly()} for API levels 29+, or a + * best-effort approximation for lower levels. + */ + private static boolean isSoftwareOnly(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isSoftwareOnlyV29(codecInfo); + } + String codecName = Util.toLowerInvariant(codecInfo.getName()); + if (codecName.startsWith("arc.")) { // App Runtime for Chrome (ARC) codecs + return false; + } + return codecName.startsWith("omx.google.") + || codecName.startsWith("omx.ffmpeg.") + || (codecName.startsWith("omx.sec.") && codecName.contains(".sw.")) + || codecName.equals("omx.qcom.video.decoder.hevcswvdec") + || codecName.startsWith("c2.android.") + || codecName.startsWith("c2.google.") + || (!codecName.startsWith("omx.") && !codecName.startsWith("c2.")); + } + + @TargetApi(29) + private static boolean isSoftwareOnlyV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isSoftwareOnly(); + } + + /** + * The result of {@link android.media.MediaCodecInfo#isVendor()} for API levels 29+, or a + * best-effort approximation for lower levels. + */ + private static boolean isVendor(android.media.MediaCodecInfo codecInfo) { + if (Util.SDK_INT >= 29) { + return isVendorV29(codecInfo); + } + String codecName = Util.toLowerInvariant(codecInfo.getName()); + return !codecName.startsWith("omx.google.") + && !codecName.startsWith("c2.android.") + && !codecName.startsWith("c2.google."); + } + + @TargetApi(29) + private static boolean isVendorV29(android.media.MediaCodecInfo codecInfo) { + return codecInfo.isVendor(); } /** @@ -517,6 +678,7 @@ public final class MediaCodecUtil { && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); } + @Nullable private static Pair getDolbyVisionProfileAndLevel( String codec, String[] parts) { if (parts.length < 3) { @@ -530,14 +692,14 @@ public final class MediaCodecUtil { Log.w(TAG, "Ignoring malformed Dolby Vision codec string: " + codec); return null; } - String profileString = matcher.group(1); - Integer profile = DOLBY_VISION_STRING_TO_PROFILE.get(profileString); + @Nullable String profileString = matcher.group(1); + @Nullable Integer profile = DOLBY_VISION_STRING_TO_PROFILE.get(profileString); if (profile == null) { Log.w(TAG, "Unknown Dolby Vision profile string: " + profileString); return null; } String levelString = parts[2]; - Integer level = DOLBY_VISION_STRING_TO_LEVEL.get(levelString); + @Nullable Integer level = DOLBY_VISION_STRING_TO_LEVEL.get(levelString); if (level == null) { Log.w(TAG, "Unknown Dolby Vision level string: " + levelString); return null; @@ -545,6 +707,7 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } + @Nullable private static Pair getHevcProfileAndLevel(String codec, String[] parts) { if (parts.length < 4) { // The codec has fewer parts than required by the HEVC codec string format. @@ -557,7 +720,7 @@ public final class MediaCodecUtil { Log.w(TAG, "Ignoring malformed HEVC codec string: " + codec); return null; } - String profileString = matcher.group(1); + @Nullable String profileString = matcher.group(1); int profile; if ("1".equals(profileString)) { profile = CodecProfileLevel.HEVCProfileMain; @@ -567,8 +730,8 @@ public final class MediaCodecUtil { Log.w(TAG, "Unknown HEVC profile string: " + profileString); return null; } - String levelString = parts[3]; - Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(levelString); + @Nullable String levelString = parts[3]; + @Nullable Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(levelString); if (level == null) { Log.w(TAG, "Unknown HEVC level string: " + levelString); return null; @@ -576,6 +739,7 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } + @Nullable private static Pair getAvcProfileAndLevel(String codec, String[] parts) { if (parts.length < 2) { // The codec has fewer parts than required by the AVC codec string format. @@ -616,6 +780,82 @@ public final class MediaCodecUtil { return new Pair<>(profile, level); } + @Nullable + private static Pair getVp9ProfileAndLevel(String codec, String[] parts) { + if (parts.length < 3) { + Log.w(TAG, "Ignoring malformed VP9 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed VP9 codec string: " + codec); + return null; + } + + int profile = VP9_PROFILE_NUMBER_TO_CONST.get(profileInteger, -1); + if (profile == -1) { + Log.w(TAG, "Unknown VP9 profile: " + profileInteger); + return null; + } + int level = VP9_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown VP9 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + + @Nullable + private static Pair getAv1ProfileAndLevel( + String codec, String[] parts, @Nullable ColorInfo colorInfo) { + if (parts.length < 4) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + int profileInteger; + int levelInteger; + int bitDepthInteger; + try { + profileInteger = Integer.parseInt(parts[1]); + levelInteger = Integer.parseInt(parts[2].substring(0, 2)); + bitDepthInteger = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed AV1 codec string: " + codec); + return null; + } + + if (profileInteger != 0) { + Log.w(TAG, "Unknown AV1 profile: " + profileInteger); + return null; + } + if (bitDepthInteger != 8 && bitDepthInteger != 10) { + Log.w(TAG, "Unknown AV1 bit depth: " + bitDepthInteger); + return null; + } + int profile; + if (bitDepthInteger == 8) { + profile = CodecProfileLevel.AV1ProfileMain8; + } else if (colorInfo != null + && (colorInfo.hdrStaticInfo != null + || colorInfo.colorTransfer == C.COLOR_TRANSFER_HLG + || colorInfo.colorTransfer == C.COLOR_TRANSFER_ST2084)) { + profile = CodecProfileLevel.AV1ProfileMain10HDR10; + } else { + profile = CodecProfileLevel.AV1ProfileMain10; + } + + int level = AV1_LEVEL_NUMBER_TO_CONST.get(levelInteger, -1); + if (level == -1) { + Log.w(TAG, "Unknown AV1 level: " + levelInteger); + return null; + } + return new Pair<>(profile, level); + } + /** * Conversion values taken from ISO 14496-10 Table A-1. * @@ -665,7 +905,7 @@ public final class MediaCodecUtil { try { // Get the object type indication, which is a hexadecimal value (see RFC 6381/ISO 14496-1). int objectTypeIndication = Integer.parseInt(parts[1], 16); - String mimeType = MimeTypes.getMimeTypeFromMp4ObjectType(objectTypeIndication); + @Nullable String mimeType = MimeTypes.getMimeTypeFromMp4ObjectType(objectTypeIndication); if (MimeTypes.AUDIO_AAC.equals(mimeType)) { // For MPEG-4 audio this is followed by an audio object type indication as a decimal number. int audioObjectTypeIndication = Integer.parseInt(parts[2]); @@ -681,6 +921,17 @@ public final class MediaCodecUtil { return null; } + /** Stably sorts the provided {@code list} in-place, in order of decreasing score. */ + private static void sortByScore(List list, ScoreProvider scoreProvider) { + Collections.sort(list, (a, b) -> scoreProvider.getScore(b) - scoreProvider.getScore(a)); + } + + /** Interface for providers of item scores. */ + private interface ScoreProvider { + /** Returns the score of the provided item. */ + int getScore(T t); + } + private interface MediaCodecListCompat { /** @@ -712,8 +963,10 @@ public final class MediaCodecUtil { private final int codecKind; - private android.media.MediaCodecInfo[] mediaCodecInfos; + @Nullable private android.media.MediaCodecInfo[] mediaCodecInfos; + // the constructor does not initialize fields: mediaCodecInfos + @SuppressWarnings("nullness:initialization.fields.uninitialized") public MediaCodecListCompatV21(boolean includeSecure, boolean includeTunneling) { codecKind = includeSecure || includeTunneling @@ -727,6 +980,8 @@ public final class MediaCodecUtil { return mediaCodecInfos.length; } + // incompatible types in return. + @SuppressWarnings("nullness:return.type.incompatible") @Override public android.media.MediaCodecInfo getCodecInfoAt(int index) { ensureMediaCodecInfosInitialized(); @@ -750,6 +1005,7 @@ public final class MediaCodecUtil { return capabilities.isFeatureRequired(feature); } + @EnsuresNonNull({"mediaCodecInfos"}) private void ensureMediaCodecInfosInitialized() { if (mediaCodecInfos == null) { mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos(); @@ -758,7 +1014,6 @@ public final class MediaCodecUtil { } - @SuppressWarnings("deprecation") private static final class MediaCodecListCompatV16 implements MediaCodecListCompat { @Override @@ -809,7 +1064,7 @@ public final class MediaCodecUtil { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode()); + result = prime * result + mimeType.hashCode(); result = prime * result + (secure ? 1231 : 1237); result = prime * result + (tunneling ? 1231 : 1237); return result; @@ -831,44 +1086,6 @@ public final class MediaCodecUtil { } - /** - * Comparator for ordering media codecs that handle {@link MimeTypes#AUDIO_RAW} to work around - * possible inconsistent behavior across different devices. A list sorted with this comparator has - * more preferred codecs first. - */ - private static final class RawAudioCodecComparator implements Comparator { - @Override - public int compare(MediaCodecInfo a, MediaCodecInfo b) { - return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b); - } - - private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) { - String name = mediaCodecInfo.name; - if (name.startsWith("OMX.google") || name.startsWith("c2.android")) { - // Prefer generic decoders over ones provided by the device. - return -1; - } - if (Util.SDK_INT < 26 && name.equals("OMX.MTK.AUDIO.DECODER.RAW")) { - // This decoder may modify the audio, so any other compatible decoders take precedence. See - // [Internal: b/62337687]. - return 1; - } - return 0; - } - } - - /** Comparator for preferring OMX.google media codecs. */ - private static final class PreferOmxGoogleCodecComparator implements Comparator { - @Override - public int compare(MediaCodecInfo a, MediaCodecInfo b) { - return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b); - } - - private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) { - return mediaCodecInfo.name.startsWith("OMX.google") ? -1 : 0; - } - } - static { AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline); @@ -898,6 +1115,26 @@ public final class MediaCodecUtil { AVC_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.AVCLevel51); AVC_LEVEL_NUMBER_TO_CONST.put(52, CodecProfileLevel.AVCLevel52); + VP9_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); + VP9_PROFILE_NUMBER_TO_CONST.put(0, CodecProfileLevel.VP9Profile0); + VP9_PROFILE_NUMBER_TO_CONST.put(1, CodecProfileLevel.VP9Profile1); + VP9_PROFILE_NUMBER_TO_CONST.put(2, CodecProfileLevel.VP9Profile2); + VP9_PROFILE_NUMBER_TO_CONST.put(3, CodecProfileLevel.VP9Profile3); + VP9_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + VP9_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.VP9Level1); + VP9_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.VP9Level11); + VP9_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.VP9Level2); + VP9_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.VP9Level21); + VP9_LEVEL_NUMBER_TO_CONST.put(30, CodecProfileLevel.VP9Level3); + VP9_LEVEL_NUMBER_TO_CONST.put(31, CodecProfileLevel.VP9Level31); + VP9_LEVEL_NUMBER_TO_CONST.put(40, CodecProfileLevel.VP9Level4); + VP9_LEVEL_NUMBER_TO_CONST.put(41, CodecProfileLevel.VP9Level41); + VP9_LEVEL_NUMBER_TO_CONST.put(50, CodecProfileLevel.VP9Level5); + VP9_LEVEL_NUMBER_TO_CONST.put(51, CodecProfileLevel.VP9Level51); + VP9_LEVEL_NUMBER_TO_CONST.put(60, CodecProfileLevel.VP9Level6); + VP9_LEVEL_NUMBER_TO_CONST.put(61, CodecProfileLevel.VP9Level61); + VP9_LEVEL_NUMBER_TO_CONST.put(62, CodecProfileLevel.VP9Level62); + HEVC_CODEC_STRING_TO_PROFILE_LEVEL = new HashMap<>(); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L30", CodecProfileLevel.HEVCMainTierLevel1); HEVC_CODEC_STRING_TO_PROFILE_LEVEL.put("L60", CodecProfileLevel.HEVCMainTierLevel2); @@ -950,6 +1187,34 @@ public final class MediaCodecUtil { DOLBY_VISION_STRING_TO_LEVEL.put("08", CodecProfileLevel.DolbyVisionLevelUhd48); DOLBY_VISION_STRING_TO_LEVEL.put("09", CodecProfileLevel.DolbyVisionLevelUhd60); + // See https://aomediacodec.github.io/av1-spec/av1-spec.pdf Annex A: Profiles and levels for + // more information on mapping AV1 codec strings to levels. + AV1_LEVEL_NUMBER_TO_CONST = new SparseIntArray(); + AV1_LEVEL_NUMBER_TO_CONST.put(0, CodecProfileLevel.AV1Level2); + AV1_LEVEL_NUMBER_TO_CONST.put(1, CodecProfileLevel.AV1Level21); + AV1_LEVEL_NUMBER_TO_CONST.put(2, CodecProfileLevel.AV1Level22); + AV1_LEVEL_NUMBER_TO_CONST.put(3, CodecProfileLevel.AV1Level23); + AV1_LEVEL_NUMBER_TO_CONST.put(4, CodecProfileLevel.AV1Level3); + AV1_LEVEL_NUMBER_TO_CONST.put(5, CodecProfileLevel.AV1Level31); + AV1_LEVEL_NUMBER_TO_CONST.put(6, CodecProfileLevel.AV1Level32); + AV1_LEVEL_NUMBER_TO_CONST.put(7, CodecProfileLevel.AV1Level33); + AV1_LEVEL_NUMBER_TO_CONST.put(8, CodecProfileLevel.AV1Level4); + AV1_LEVEL_NUMBER_TO_CONST.put(9, CodecProfileLevel.AV1Level41); + AV1_LEVEL_NUMBER_TO_CONST.put(10, CodecProfileLevel.AV1Level42); + AV1_LEVEL_NUMBER_TO_CONST.put(11, CodecProfileLevel.AV1Level43); + AV1_LEVEL_NUMBER_TO_CONST.put(12, CodecProfileLevel.AV1Level5); + AV1_LEVEL_NUMBER_TO_CONST.put(13, CodecProfileLevel.AV1Level51); + AV1_LEVEL_NUMBER_TO_CONST.put(14, CodecProfileLevel.AV1Level52); + AV1_LEVEL_NUMBER_TO_CONST.put(15, CodecProfileLevel.AV1Level53); + AV1_LEVEL_NUMBER_TO_CONST.put(16, CodecProfileLevel.AV1Level6); + AV1_LEVEL_NUMBER_TO_CONST.put(17, CodecProfileLevel.AV1Level61); + AV1_LEVEL_NUMBER_TO_CONST.put(18, CodecProfileLevel.AV1Level62); + AV1_LEVEL_NUMBER_TO_CONST.put(19, CodecProfileLevel.AV1Level63); + AV1_LEVEL_NUMBER_TO_CONST.put(20, CodecProfileLevel.AV1Level7); + AV1_LEVEL_NUMBER_TO_CONST.put(21, CodecProfileLevel.AV1Level71); + AV1_LEVEL_NUMBER_TO_CONST.put(22, CodecProfileLevel.AV1Level72); + AV1_LEVEL_NUMBER_TO_CONST.put(23, CodecProfileLevel.AV1Level73); + MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE = new SparseIntArray(); MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(1, CodecProfileLevel.AACObjectMain); MP4A_AUDIO_OBJECT_TYPE_TO_PROFILE.put(2, CodecProfileLevel.AACObjectLC); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/package-info.java new file mode 100644 index 000000000..b09404a6f --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.mediacodec; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java index 35702da57..046c1fef5 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/Metadata.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.List; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A collection of metadata entries. @@ -57,19 +56,15 @@ public final class Metadata implements Parcelable { * @param entries The metadata entries. */ public Metadata(Entry... entries) { - this.entries = entries == null ? new Entry[0] : entries; + this.entries = entries; } /** * @param entries The metadata entries. */ public Metadata(List entries) { - if (entries != null) { - this.entries = new Entry[entries.size()]; - entries.toArray(this.entries); - } else { - this.entries = new Entry[0]; - } + this.entries = new Entry[entries.size()]; + entries.toArray(this.entries); } /* package */ Metadata(Parcel in) { @@ -118,9 +113,10 @@ public final class Metadata implements Parcelable { * @return The metadata instance with the appended entries. */ public Metadata copyWithAppendedEntries(Entry... entriesToAppend) { - @NullableType Entry[] merged = Arrays.copyOf(entries, entries.length + entriesToAppend.length); - System.arraycopy(entriesToAppend, 0, merged, entries.length, entriesToAppend.length); - return new Metadata(Util.castNonNullTypeArray(merged)); + if (entriesToAppend.length == 0) { + return this; + } + return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend)); } @Override diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java index ae4b7db5c..0b653830a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; import com.google.android.exoplayer2.metadata.icy.IcyDecoder; @@ -62,7 +63,7 @@ public interface MetadataDecoderFactory { @Override public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; + @Nullable String mimeType = format.sampleMimeType; return MimeTypes.APPLICATION_ID3.equals(mimeType) || MimeTypes.APPLICATION_EMSG.equals(mimeType) || MimeTypes.APPLICATION_SCTE35.equals(mimeType) @@ -71,19 +72,23 @@ public interface MetadataDecoderFactory { @Override public MetadataDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.APPLICATION_ID3: - return new Id3Decoder(); - case MimeTypes.APPLICATION_EMSG: - return new EventMessageDecoder(); - case MimeTypes.APPLICATION_SCTE35: - return new SpliceInfoDecoder(); - case MimeTypes.APPLICATION_ICY: - return new IcyDecoder(); - default: - throw new IllegalArgumentException( - "Attempted to create decoder for unsupported format"); + @Nullable String mimeType = format.sampleMimeType; + if (mimeType != null) { + switch (mimeType) { + case MimeTypes.APPLICATION_ID3: + return new Id3Decoder(); + case MimeTypes.APPLICATION_EMSG: + return new EventMessageDecoder(); + case MimeTypes.APPLICATION_SCTE35: + return new SpliceInfoDecoder(); + case MimeTypes.APPLICATION_ICY: + return new IcyDecoder(); + default: + break; + } } + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported MIME type: " + mimeType); } }; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index be965bd48..7a5235a46 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.metadata; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; @@ -22,26 +24,21 @@ import android.os.Message; import androidx.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A renderer for metadata. */ public final class MetadataRenderer extends BaseRenderer implements Callback { - /** - * @deprecated Use {@link MetadataOutput}. - */ - @Deprecated - public interface Output extends MetadataOutput {} - private static final int MSG_INVOKE_RENDERER = 0; // TODO: Holding multiple pending metadata objects is temporary mitigation against // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been @@ -50,15 +47,14 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private final MetadataDecoderFactory decoderFactory; private final MetadataOutput output; - private final @Nullable Handler outputHandler; - private final FormatHolder formatHolder; + @Nullable private final Handler outputHandler; private final MetadataInputBuffer buffer; - private final Metadata[] pendingMetadata; + private final @NullableType Metadata[] pendingMetadata; private final long[] pendingMetadataTimestamps; private int pendingMetadataIndex; private int pendingMetadataCount; - private MetadataDecoder decoder; + @Nullable private MetadataDecoder decoder; private boolean inputStreamEnded; private long subsampleOffsetUs; @@ -90,23 +86,24 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { this.outputHandler = outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this); this.decoderFactory = Assertions.checkNotNull(decoderFactory); - formatHolder = new FormatHolder(); buffer = new MetadataInputBuffer(); pendingMetadata = new Metadata[MAX_PENDING_METADATA_COUNT]; pendingMetadataTimestamps = new long[MAX_PENDING_METADATA_COUNT]; } @Override + @Capabilities public int supportsFormat(Format format) { if (decoderFactory.supportsFormat(format)) { - return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create( + supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); } else { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } } @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { decoder = decoderFactory.createDecoder(formats[0]); } @@ -117,9 +114,10 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (!inputStreamEnded && pendingMetadataCount < MAX_PENDING_METADATA_COUNT) { buffer.clear(); + FormatHolder formatHolder = getFormatHolder(); int result = readSource(formatHolder, buffer, false); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { @@ -131,7 +129,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } else { buffer.subsampleOffsetUs = subsampleOffsetUs; buffer.flip(); - Metadata metadata = decoder.decode(buffer); + @Nullable Metadata metadata = castNonNull(decoder).decode(buffer); if (metadata != null) { List entries = new ArrayList<>(metadata.length()); decodeWrappedMetadata(metadata, entries); @@ -146,12 +144,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { } } } else if (result == C.RESULT_FORMAT_READ) { - subsampleOffsetUs = formatHolder.format.subsampleOffsetUs; + subsampleOffsetUs = Assertions.checkNotNull(formatHolder.format).subsampleOffsetUs; } } if (pendingMetadataCount > 0 && pendingMetadataTimestamps[pendingMetadataIndex] <= positionUs) { - invokeRenderer(pendingMetadata[pendingMetadataIndex]); + Metadata metadata = castNonNull(pendingMetadata[pendingMetadataIndex]); + invokeRenderer(metadata); pendingMetadata[pendingMetadataIndex] = null; pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; pendingMetadataCount--; @@ -165,7 +164,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { */ private void decodeWrappedMetadata(Metadata metadata, List decodedEntries) { for (int i = 0; i < metadata.length(); i++) { - Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); + @Nullable Format wrappedMetadataFormat = metadata.get(i).getWrappedMetadataFormat(); if (wrappedMetadataFormat != null && decoderFactory.supportsFormat(wrappedMetadataFormat)) { MetadataDecoder wrappedMetadataDecoder = decoderFactory.createDecoder(wrappedMetadataFormat); @@ -174,7 +173,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { Assertions.checkNotNull(metadata.get(i).getWrappedMetadataBytes()); buffer.clear(); buffer.ensureSpaceForWrite(wrappedMetadataBytes.length); - buffer.data.put(wrappedMetadataBytes); + castNonNull(buffer.data).put(wrappedMetadataBytes); buffer.flip(); @Nullable Metadata innerMetadata = wrappedMetadataDecoder.decode(buffer); if (innerMetadata != null) { @@ -218,7 +217,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { pendingMetadataCount = 0; } - @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { switch (msg.what) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java index d4e254f95..d87376feb 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -29,31 +28,20 @@ public final class EventMessageDecoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override - @Nullable public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = inputBuffer.data; + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); byte[] data = buffer.array(); int size = buffer.limit(); - EventMessage decodedEventMessage = decode(new ParsableByteArray(data, size)); - if (decodedEventMessage == null) { - return null; - } else { - return new Metadata(decodedEventMessage); - } + return new Metadata(decode(new ParsableByteArray(data, size))); } - @Nullable public EventMessage decode(ParsableByteArray emsgData) { - try { - String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); - long durationMs = emsgData.readUnsignedInt(); - long id = emsgData.readUnsignedInt(); - byte[] messageData = - Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); - return new EventMessage(schemeIdUri, value, durationMs, id, messageData); - } catch (RuntimeException e) { - return null; - } + String schemeIdUri = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + String value = Assertions.checkNotNull(emsgData.readNullTerminatedString()); + long durationMs = emsgData.readUnsignedInt(); + long id = emsgData.readUnsignedInt(); + byte[] messageData = + Arrays.copyOfRange(emsgData.data, emsgData.getPosition(), emsgData.limit()); + return new EventMessage(schemeIdUri, value, durationMs, id, messageData); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java new file mode 100644 index 000000000..2b03ce8df --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/emsg/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.emsg; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java new file mode 100644 index 000000000..343ab232e --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/flac/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.flac; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java index a148c03b6..854a8fc3a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyDecoder.java @@ -15,42 +15,54 @@ */ package com.google.android.exoplayer2.metadata.icy; -import androidx.annotation.VisibleForTesting; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.util.regex.Matcher; import java.util.regex.Pattern; /** Decodes ICY stream information. */ public final class IcyDecoder implements MetadataDecoder { - private static final String TAG = "IcyDecoder"; - private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL); private static final String STREAM_KEY_NAME = "streamtitle"; private static final String STREAM_KEY_URL = "streamurl"; + private final CharsetDecoder utf8Decoder; + private final CharsetDecoder iso88591Decoder; + + public IcyDecoder() { + utf8Decoder = Charset.forName(C.UTF8_NAME).newDecoder(); + iso88591Decoder = Charset.forName(C.ISO88591_NAME).newDecoder(); + } + @Override @SuppressWarnings("ByteBufferBackingArray") public Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = inputBuffer.data; - byte[] data = buffer.array(); - int length = buffer.limit(); - return decode(Util.fromUtf8Bytes(data, 0, length)); - } + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + @Nullable String icyString = decodeToString(buffer); + byte[] icyBytes = new byte[buffer.limit()]; + buffer.get(icyBytes); - @VisibleForTesting - /* package */ Metadata decode(String metadata) { - String name = null; - String url = null; + if (icyString == null) { + return new Metadata(new IcyInfo(icyBytes, /* title= */ null, /* url= */ null)); + } + + @Nullable String name = null; + @Nullable String url = null; int index = 0; - Matcher matcher = METADATA_ELEMENT.matcher(metadata); + Matcher matcher = METADATA_ELEMENT.matcher(icyString); while (matcher.find(index)) { - String key = Util.toLowerInvariant(matcher.group(1)); - String value = matcher.group(2); + @Nullable String key = Util.toLowerInvariant(matcher.group(1)); + @Nullable String value = matcher.group(2); switch (key) { case STREAM_KEY_NAME: name = value; @@ -61,6 +73,29 @@ public final class IcyDecoder implements MetadataDecoder { } index = matcher.end(); } - return new Metadata(new IcyInfo(metadata, name, url)); + return new Metadata(new IcyInfo(icyBytes, name, url)); + } + + // The ICY spec doesn't specify a character encoding, and there's no way to communicate one + // either. So try decoding UTF-8 first, then fall back to ISO-8859-1. + // https://github.com/google/ExoPlayer/issues/6753 + @Nullable + private String decodeToString(ByteBuffer data) { + try { + return utf8Decoder.decode(data).toString(); + } catch (CharacterCodingException e) { + // Fall through to try ISO-8859-1 decoding. + } finally { + utf8Decoder.reset(); + data.rewind(); + } + try { + return iso88591Decoder.decode(data).toString(); + } catch (CharacterCodingException e) { + return null; + } finally { + iso88591Decoder.reset(); + data.rewind(); + } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java index 1198d1af8..1a3ed2ea6 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/IcyInfo.java @@ -20,34 +20,34 @@ import android.os.Parcelable; import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; /** ICY in-stream information. */ public final class IcyInfo implements Metadata.Entry { - /** The complete metadata string used to construct this IcyInfo. */ - public final String rawMetadata; - /** The stream title if present, or {@code null}. */ + /** The complete metadata bytes used to construct this IcyInfo. */ + public final byte[] rawMetadata; + /** The stream title if present and decodable, or {@code null}. */ @Nullable public final String title; - /** The stream URL if present, or {@code null}. */ + /** The stream URL if present and decodable, or {@code null}. */ @Nullable public final String url; /** - * Construct a new IcyInfo from the source metadata string, and optionally a StreamTitle and - * StreamUrl that have been extracted. + * Construct a new IcyInfo from the source metadata, and optionally a StreamTitle and StreamUrl + * that have been extracted. * * @param rawMetadata See {@link #rawMetadata}. * @param title See {@link #title}. * @param url See {@link #url}. */ - public IcyInfo(String rawMetadata, @Nullable String title, @Nullable String url) { + public IcyInfo(byte[] rawMetadata, @Nullable String title, @Nullable String url) { this.rawMetadata = rawMetadata; this.title = title; this.url = url; } /* package */ IcyInfo(Parcel in) { - rawMetadata = Assertions.checkNotNull(in.readString()); + rawMetadata = Assertions.checkNotNull(in.createByteArray()); title = in.readString(); url = in.readString(); } @@ -62,26 +62,26 @@ public final class IcyInfo implements Metadata.Entry { } IcyInfo other = (IcyInfo) obj; // title & url are derived from rawMetadata, so no need to include them in the comparison. - return Util.areEqual(rawMetadata, other.rawMetadata); + return Arrays.equals(rawMetadata, other.rawMetadata); } @Override public int hashCode() { // title & url are derived from rawMetadata, so no need to include them in the hash. - return rawMetadata.hashCode(); + return Arrays.hashCode(rawMetadata); } @Override public String toString() { return String.format( - "ICY: title=\"%s\", url=\"%s\", rawMetadata=\"%s\"", title, url, rawMetadata); + "ICY: title=\"%s\", url=\"%s\", rawMetadata.length=\"%s\"", title, url, rawMetadata.length); } // Parcelable implementation. @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(rawMetadata); + dest.writeByteArray(rawMetadata); dest.writeString(title); dest.writeString(url); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java new file mode 100644 index 000000000..2a2d0c7fc --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/icy/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.icy; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java index c233ad61b..3f4a40067 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/ApicFrame.java @@ -31,7 +31,7 @@ public final class ApicFrame extends Id3Frame { public static final String ID = "APIC"; public final String mimeType; - public final @Nullable String description; + @Nullable public final String description; public final int pictureType; public final byte[] pictureData; @@ -47,7 +47,7 @@ public final class ApicFrame extends Id3Frame { /* package */ ApicFrame(Parcel in) { super(ID); mimeType = castNonNull(in.readString()); - description = castNonNull(in.readString()); + description = in.readString(); pictureType = in.readInt(); pictureData = castNonNull(in.createByteArray()); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java index fff0828b3..faab7f077 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -61,10 +62,8 @@ public final class Id3Decoder implements MetadataDecoder { private static final String TAG = "Id3Decoder"; - /** - * The first three bytes of a well formed ID3 tag header. - */ - public static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** The first three bytes of a well formed ID3 tag header. */ + public static final int ID3_TAG = 0x00494433; /** * Length of an ID3 tag header. */ @@ -84,7 +83,7 @@ public final class Id3Decoder implements MetadataDecoder { private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; private static final int ID3_TEXT_ENCODING_UTF_8 = 3; - private final @Nullable FramePredicate framePredicate; + @Nullable private final FramePredicate framePredicate; public Id3Decoder() { this(null); @@ -99,8 +98,9 @@ public final class Id3Decoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override - public @Nullable Metadata decode(MetadataInputBuffer inputBuffer) { - ByteBuffer buffer = inputBuffer.data; + @Nullable + public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); return decode(buffer.array(), buffer.limit()); } @@ -112,7 +112,8 @@ public final class Id3Decoder implements MetadataDecoder { * @return A {@link Metadata} object containing the decoded ID3 tags, or null if the data could * not be decoded. */ - public @Nullable Metadata decode(byte[] data, int size) { + @Nullable + public Metadata decode(byte[] data, int size) { List id3Frames = new ArrayList<>(); ParsableByteArray id3Data = new ParsableByteArray(data, size); @@ -154,7 +155,8 @@ public final class Id3Decoder implements MetadataDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. * @return The parsed header, or null if the ID3 tag is unsupported. */ - private static @Nullable Id3Header decodeHeader(ParsableByteArray data) { + @Nullable + private static Id3Header decodeHeader(ParsableByteArray data) { if (data.bytesLeft() < ID3_HEADER_LENGTH) { Log.w(TAG, "Data too short to be an ID3 tag"); return null; @@ -162,7 +164,7 @@ public final class Id3Decoder implements MetadataDecoder { int id = data.readUnsignedInt24(); if (id != ID3_TAG) { - Log.w(TAG, "Unexpected first three bytes of ID3 tag header: " + id); + Log.w(TAG, "Unexpected first three bytes of ID3 tag header: 0x" + String.format("%06X", id)); return null; } @@ -268,7 +270,8 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static @Nullable Id3Frame decodeFrame( + @Nullable + private static Id3Frame decodeFrame( int majorVersion, ParsableByteArray id3Data, boolean unsignedIntFrameSizeHack, @@ -403,8 +406,9 @@ public final class Id3Decoder implements MetadataDecoder { } } - private static @Nullable TextInformationFrame decodeTxxxFrame( - ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { + @Nullable + private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. return null; @@ -426,7 +430,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame("TXXX", description, value); } - private static @Nullable TextInformationFrame decodeTextInformationFrame( + @Nullable + private static TextInformationFrame decodeTextInformationFrame( ParsableByteArray id3Data, int frameSize, String id) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -445,7 +450,8 @@ public final class Id3Decoder implements MetadataDecoder { return new TextInformationFrame(id, null, value); } - private static @Nullable UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) + @Nullable + private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 1) { // Frame is malformed. @@ -556,7 +562,8 @@ public final class Id3Decoder implements MetadataDecoder { return new ApicFrame(mimeType, description, pictureType, pictureData); } - private static @Nullable CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) + @Nullable + private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize) throws UnsupportedEncodingException { if (frameSize < 4) { // Frame is malformed. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java index 5dd5280e7..8337911c0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.Util; */ public final class TextInformationFrame extends Id3Frame { - public final @Nullable String description; + @Nullable public final String description; public final String value; public TextInformationFrame(String id, @Nullable String description, String value) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java index 8be9ed188..298558b66 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.Util; */ public final class UrlLinkFrame extends Id3Frame { - public final @Nullable String description; + @Nullable public final String description; public final String url; public UrlLinkFrame(String id, @Nullable String description, String url) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java new file mode 100644 index 000000000..842207184 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/id3/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.id3; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/package-info.java new file mode 100644 index 000000000..a55cc1b6b --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java index 4334fa99c..44850b720 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.metadata.scte35; import android.os.Parcel; import android.os.Parcelable; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; /** * Represents a private command as defined in SCTE35, Section 9.3.6. @@ -46,8 +47,7 @@ public final class PrivateCommand extends SpliceCommand { private PrivateCommand(Parcel in) { ptsAdjustment = in.readLong(); identifier = in.readLong(); - commandBytes = new byte[in.readInt()]; - in.readByteArray(commandBytes); + commandBytes = Util.castNonNull(in.createByteArray()); } /* package */ static PrivateCommand parseFromSection(ParsableByteArray sectionData, @@ -64,7 +64,6 @@ public final class PrivateCommand extends SpliceCommand { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(ptsAdjustment); dest.writeLong(identifier); - dest.writeInt(commandBytes.length); dest.writeByteArray(commandBytes); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java index 1153f918f..0e161d9c6 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java @@ -15,13 +15,16 @@ */ package com.google.android.exoplayer2.metadata.scte35; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Decodes splice info sections and produces splice commands. @@ -37,7 +40,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { private final ParsableByteArray sectionData; private final ParsableBitArray sectionHeader; - private TimestampAdjuster timestampAdjuster; + @MonotonicNonNull private TimestampAdjuster timestampAdjuster; public SpliceInfoDecoder() { sectionData = new ParsableByteArray(); @@ -47,6 +50,8 @@ public final class SpliceInfoDecoder implements MetadataDecoder { @SuppressWarnings("ByteBufferBackingArray") @Override public Metadata decode(MetadataInputBuffer inputBuffer) { + ByteBuffer buffer = Assertions.checkNotNull(inputBuffer.data); + // Internal timestamps adjustment. if (timestampAdjuster == null || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) { @@ -54,7 +59,6 @@ public final class SpliceInfoDecoder implements MetadataDecoder { timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs); } - ByteBuffer buffer = inputBuffer.data; byte[] data = buffer.array(); int size = buffer.limit(); sectionData.reset(data, size); @@ -68,7 +72,7 @@ public final class SpliceInfoDecoder implements MetadataDecoder { sectionHeader.skipBits(20); int spliceCommandLength = sectionHeader.readBits(12); int spliceCommandType = sectionHeader.readBits(8); - SpliceCommand command = null; + @Nullable SpliceCommand command = null; // Go to the start of the command by skipping all fields up to command_type. sectionData.skipBytes(14); switch (spliceCommandType) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java new file mode 100644 index 000000000..0c4448f4d --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/metadata/scte35/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.metadata.scte35; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java index a05318543..c69908c74 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -68,7 +68,7 @@ import java.util.List; if (!exists()) { return new DownloadRequest[0]; } - InputStream inputStream = null; + @Nullable InputStream inputStream = null; try { inputStream = atomicFile.openRead(); DataInputStream dataInputStream = new DataInputStream(inputStream); @@ -99,7 +99,7 @@ import java.util.List; boolean isRemoveAction = input.readBoolean(); int dataLength = input.readInt(); - byte[] data; + @Nullable byte[] data; if (dataLength != 0) { data = new byte[dataLength]; input.readFully(data); @@ -123,7 +123,7 @@ import java.util.List; && (DownloadRequest.TYPE_DASH.equals(type) || DownloadRequest.TYPE_HLS.equals(type) || DownloadRequest.TYPE_SS.equals(type)); - String customCacheKey = null; + @Nullable String customCacheKey = null; if (!isLegacySegmented) { customCacheKey = input.readBoolean() ? input.readUTF() : null; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java index baf47772a..999059e85 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ActionFileUpgradeUtil.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.offline; import static com.google.android.exoplayer2.offline.Download.STATE_QUEUED; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; import java.io.File; import java.io.IOException; @@ -47,6 +48,8 @@ public final class ActionFileUpgradeUtil { *

This method must not be called while the {@link DefaultDownloadIndex} is being used by a * {@link DownloadManager}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param actionFilePath The action file path. * @param downloadIdProvider A download ID provider, or {@code null}. If {@code null} then ID of * each download will be its custom cache key if one is specified, or else its URL. @@ -55,6 +58,7 @@ public final class ActionFileUpgradeUtil { * @param addNewDownloadsAsCompleted Whether to add new downloads as completed. * @throws IOException If an error occurs loading or merging the requests. */ + @WorkerThread @SuppressWarnings("deprecation") public static void upgradeAndDelete( File actionFilePath, @@ -97,7 +101,7 @@ public final class ActionFileUpgradeUtil { boolean addNewDownloadAsCompleted, long nowMs) throws IOException { - Download download = downloadIndex.getDownload(request.id); + @Nullable Download download = downloadIndex.getDownload(request.id); if (download != null) { download = DownloadManager.mergeRequest(download, request, download.stopReason, nowMs); } else { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index ef4bd00f2..f1c897813 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -26,6 +26,8 @@ import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; +import com.google.android.exoplayer2.offline.Download.FailureReason; +import com.google.android.exoplayer2.offline.Download.State; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -239,6 +241,9 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { try { ContentValues values = new ContentValues(); values.put(COLUMN_STATE, Download.STATE_REMOVING); + // Only downloads in STATE_FAILED are allowed a failure reason, so we need to clear it here in + // case we're moving downloads from STATE_FAILED to STATE_REMOVING. + values.put(COLUMN_FAILURE_REASON, Download.FAILURE_REASON_NONE); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null); } catch (SQLException e) { @@ -285,7 +290,7 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE, name); if (version != TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { VersionTable.setVersion( writableDatabase, VersionTable.FEATURE_OFFLINE, name, TABLE_VERSION); @@ -302,6 +307,8 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { } } + // incompatible types in argument. + @SuppressWarnings("nullness:argument.type.incompatible") private Cursor getCursor(String selection, @Nullable String[] selectionArgs) throws DatabaseIOException { try { @@ -349,14 +356,22 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex { DownloadProgress downloadProgress = new DownloadProgress(); downloadProgress.bytesDownloaded = cursor.getLong(COLUMN_INDEX_BYTES_DOWNLOADED); downloadProgress.percentDownloaded = cursor.getFloat(COLUMN_INDEX_PERCENT_DOWNLOADED); + @State int state = cursor.getInt(COLUMN_INDEX_STATE); + // It's possible the database contains failure reasons for non-failed downloads, which is + // invalid. Clear them here. See https://github.com/google/ExoPlayer/issues/6785. + @FailureReason + int failureReason = + state == Download.STATE_FAILED + ? cursor.getInt(COLUMN_INDEX_FAILURE_REASON) + : Download.FAILURE_REASON_NONE; return new Download( request, - /* state= */ cursor.getInt(COLUMN_INDEX_STATE), + state, /* startTimeMs= */ cursor.getLong(COLUMN_INDEX_START_TIME_MS), /* updateTimeMs= */ cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), /* contentLength= */ cursor.getLong(COLUMN_INDEX_CONTENT_LENGTH), /* stopReason= */ cursor.getInt(COLUMN_INDEX_STOP_REASON), - /* failureReason= */ cursor.getInt(COLUMN_INDEX_FAILURE_REASON), + failureReason, downloadProgress); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/Download.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/Download.java index 97dff8394..da46120b2 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/Download.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/Download.java @@ -130,9 +130,9 @@ public final class Download { @FailureReason int failureReason, DownloadProgress progress) { Assertions.checkNotNull(progress); - Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); + Assertions.checkArgument((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); if (stopReason != 0) { - Assertions.checkState(state != STATE_DOWNLOADING && state != STATE_QUEUED); + Assertions.checkArgument(state != STATE_DOWNLOADING && state != STATE_QUEUED); } this.request = request; this.state = state; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 821696aae..6707c1e49 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -15,12 +15,13 @@ */ package com.google.android.exoplayer2.offline; +import android.content.Context; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; -import androidx.annotation.Nullable; import android.util.SparseIntArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.RendererCapabilities; @@ -31,9 +32,13 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; @@ -51,7 +56,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -82,11 +86,39 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public final class DownloadHelper { /** - * The default parameters used for track selection for downloading. This default selects the - * highest bitrate audio and video tracks which are supported by the renderers. + * Default track selection parameters for downloading, but without any {@link Context} + * constraints. + * + *

If possible, use {@link #getDefaultTrackSelectorParameters(Context)} instead. + * + * @see Parameters#DEFAULT_WITHOUT_CONTEXT */ + public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT = + Parameters.DEFAULT_WITHOUT_CONTEXT.buildUpon().setForceHighestSupportedBitrate(true).build(); + + /** + * @deprecated This instance does not have {@link Context} constraints. Use {@link + * #getDefaultTrackSelectorParameters(Context)} instead. + */ + @Deprecated + public static final Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT = + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT; + + /** + * @deprecated This instance does not have {@link Context} constraints. Use {@link + * #getDefaultTrackSelectorParameters(Context)} instead. + */ + @Deprecated public static final DefaultTrackSelector.Parameters DEFAULT_TRACK_SELECTOR_PARAMETERS = - new DefaultTrackSelector.ParametersBuilder().setForceHighestSupportedBitrate(true).build(); + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT; + + /** Returns the default parameters used for track selection for downloading. */ + public static DefaultTrackSelector.Parameters getDefaultTrackSelectorParameters(Context context) { + return Parameters.getDefaults(context) + .buildUpon() + .setForceHighestSupportedBitrate(true) + .build(); + } /** A callback to be notified when the {@link DownloadHelper} is prepared. */ public interface Callback { @@ -107,20 +139,24 @@ public final class DownloadHelper { void onPrepareError(DownloadHelper helper, IOException e); } - private static final MediaSourceFactory DASH_FACTORY = - getMediaSourceFactory("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); - private static final MediaSourceFactory SS_FACTORY = - getMediaSourceFactory( - "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); - private static final MediaSourceFactory HLS_FACTORY = - getMediaSourceFactory("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); + /** Thrown at an attempt to download live content. */ + public static class LiveContentUnsupportedException extends IOException {} - /** - * Creates a {@link DownloadHelper} for progressive streams. - * - * @param uri A stream {@link Uri}. - * @return A {@link DownloadHelper} for progressive streams. - */ + @Nullable + private static final Constructor DASH_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); + + @Nullable + private static final Constructor SS_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); + + @Nullable + private static final Constructor HLS_FACTORY_CONSTRUCTOR = + getConstructor("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); + + /** @deprecated Use {@link #forProgressive(Context, Uri)} */ + @Deprecated + @SuppressWarnings("deprecation") public static DownloadHelper forProgressive(Uri uri) { return forProgressive(uri, /* cacheKey= */ null); } @@ -128,23 +164,60 @@ public final class DownloadHelper { /** * Creates a {@link DownloadHelper} for progressive streams. * + * @param context Any {@link Context}. * @param uri A stream {@link Uri}. - * @param cacheKey An optional cache key. * @return A {@link DownloadHelper} for progressive streams. */ + public static DownloadHelper forProgressive(Context context, Uri uri) { + return forProgressive(context, uri, /* cacheKey= */ null); + } + + /** @deprecated Use {@link #forProgressive(Context, Uri, String)} */ + @Deprecated public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) { return new DownloadHelper( DownloadRequest.TYPE_PROGRESSIVE, uri, cacheKey, /* mediaSource= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT, /* rendererCapabilities= */ new RendererCapabilities[0]); } + /** + * Creates a {@link DownloadHelper} for progressive streams. + * + * @param context Any {@link Context}. + * @param uri A stream {@link Uri}. + * @param cacheKey An optional cache key. + * @return A {@link DownloadHelper} for progressive streams. + */ + public static DownloadHelper forProgressive(Context context, Uri uri, @Nullable String cacheKey) { + return new DownloadHelper( + DownloadRequest.TYPE_PROGRESSIVE, + uri, + cacheKey, + /* mediaSource= */ null, + getDefaultTrackSelectorParameters(context), + /* rendererCapabilities= */ new RendererCapabilities[0]); + } + + /** @deprecated Use {@link #forDash(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forDash( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forDash( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); + } + /** * Creates a {@link DownloadHelper} for DASH streams. * + * @param context Any {@link Context}. * @param uri A manifest {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -153,13 +226,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the DASH module is missing. */ public static DownloadHelper forDash( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forDash( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -169,8 +245,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for DASH streams. @@ -186,14 +262,32 @@ public final class DownloadHelper { DownloadRequest.TYPE_DASH, uri, /* cacheKey= */ null, - DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), + createMediaSourceInternal( + DASH_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), trackSelectorParameters, - Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + Util.getRendererCapabilities(renderersFactory)); + } + + /** @deprecated Use {@link #forHls(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forHls( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forHls( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } /** * Creates a {@link DownloadHelper} for HLS streams. * + * @param context Any {@link Context}. * @param uri A playlist {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -202,13 +296,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the HLS module is missing. */ public static DownloadHelper forHls( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forHls( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -218,8 +315,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for HLS streams. @@ -235,14 +332,32 @@ public final class DownloadHelper { DownloadRequest.TYPE_HLS, uri, /* cacheKey= */ null, - HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), + createMediaSourceInternal( + HLS_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), trackSelectorParameters, - Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + Util.getRendererCapabilities(renderersFactory)); + } + + /** @deprecated Use {@link #forSmoothStreaming(Context, Uri, Factory, RenderersFactory)} */ + @Deprecated + public static DownloadHelper forSmoothStreaming( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forSmoothStreaming( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT); } /** * Creates a {@link DownloadHelper} for SmoothStreaming streams. * + * @param context Any {@link Context}. * @param uri A manifest {@link Uri}. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are @@ -251,13 +366,16 @@ public final class DownloadHelper { * @throws IllegalStateException If the SmoothStreaming module is missing. */ public static DownloadHelper forSmoothStreaming( - Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + Context context, + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory) { return forSmoothStreaming( uri, dataSourceFactory, renderersFactory, /* drmSessionManager= */ null, - DEFAULT_TRACK_SELECTOR_PARAMETERS); + getDefaultTrackSelectorParameters(context)); } /** @@ -267,8 +385,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for SmoothStreaming streams. @@ -284,40 +402,63 @@ public final class DownloadHelper { DownloadRequest.TYPE_SS, uri, /* cacheKey= */ null, - SS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null), + createMediaSourceInternal( + SS_FACTORY_CONSTRUCTOR, + uri, + dataSourceFactory, + drmSessionManager, + /* streamKeys= */ null), trackSelectorParameters, - Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + Util.getRendererCapabilities(renderersFactory)); } /** - * Utility method to create a MediaSource which only contains the tracks defined in {@code + * Equivalent to {@link #createMediaSource(DownloadRequest, Factory, DrmSessionManager) + * createMediaSource(downloadRequest, dataSourceFactory, null)}. + */ + public static MediaSource createMediaSource( + DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { + return createMediaSource(downloadRequest, dataSourceFactory, /* drmSessionManager= */ null); + } + + /** + * Utility method to create a {@link MediaSource} that only exposes the tracks defined in {@code * downloadRequest}. * * @param downloadRequest A {@link DownloadRequest}. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. - * @return A MediaSource which only contains the tracks defined in {@code downloadRequest}. + * @param drmSessionManager An optional {@link DrmSessionManager} to be passed to the {@link + * MediaSource}. + * @return A {@link MediaSource} that only exposes the tracks defined in {@code downloadRequest}. */ public static MediaSource createMediaSource( - DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { - MediaSourceFactory factory; + DownloadRequest downloadRequest, + DataSource.Factory dataSourceFactory, + @Nullable DrmSessionManager drmSessionManager) { + @Nullable Constructor constructor; switch (downloadRequest.type) { case DownloadRequest.TYPE_DASH: - factory = DASH_FACTORY; + constructor = DASH_FACTORY_CONSTRUCTOR; break; case DownloadRequest.TYPE_SS: - factory = SS_FACTORY; + constructor = SS_FACTORY_CONSTRUCTOR; break; case DownloadRequest.TYPE_HLS: - factory = HLS_FACTORY; + constructor = HLS_FACTORY_CONSTRUCTOR; break; case DownloadRequest.TYPE_PROGRESSIVE: return new ProgressiveMediaSource.Factory(dataSourceFactory) + .setCustomCacheKey(downloadRequest.customCacheKey) .createMediaSource(downloadRequest.uri); default: throw new IllegalStateException("Unsupported type: " + downloadRequest.type); } - return factory.createMediaSource( - downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys); + return createMediaSourceInternal( + constructor, + downloadRequest.uri, + dataSourceFactory, + drmSessionManager, + downloadRequest.streamKeys); } private final String downloadType; @@ -328,6 +469,7 @@ public final class DownloadHelper { private final RendererCapabilities[] rendererCapabilities; private final SparseIntArray scratchSet; private final Handler callbackHandler; + private final Timeline.Window window; private boolean isPreparedWithMedia; private @MonotonicNonNull Callback callback; @@ -361,12 +503,13 @@ public final class DownloadHelper { this.uri = uri; this.cacheKey = cacheKey; this.mediaSource = mediaSource; - this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory()); + this.trackSelector = + new DefaultTrackSelector(trackSelectorParameters, new DownloadTrackSelection.Factory()); this.rendererCapabilities = rendererCapabilities; this.scratchSet = new SparseIntArray(); - trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); callbackHandler = new Handler(Util.getLooper()); + window = new Timeline.Window(); } /** @@ -402,7 +545,9 @@ public final class DownloadHelper { return null; } assertPreparedWithMedia(); - return mediaPreparer.manifest; + return mediaPreparer.timeline.getWindowCount() > 0 + ? mediaPreparer.timeline.getWindow(/* windowIndex= */ 0, window).manifest + : null; } /** @@ -511,7 +656,7 @@ public final class DownloadHelper { assertPreparedWithMedia(); for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { DefaultTrackSelector.ParametersBuilder parametersBuilder = - DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon(); + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { @@ -541,7 +686,7 @@ public final class DownloadHelper { assertPreparedWithMedia(); for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { DefaultTrackSelector.ParametersBuilder parametersBuilder = - DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon(); + DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT.buildUpon(); MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { @@ -713,7 +858,7 @@ public final class DownloadHelper { new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)), mediaPreparer.timeline); for (int i = 0; i < trackSelectorResult.length; i++) { - TrackSelection newSelection = trackSelectorResult.selections.get(i); + @Nullable TrackSelection newSelection = trackSelectorResult.selections.get(i); if (newSelection == null) { continue; } @@ -752,59 +897,48 @@ public final class DownloadHelper { } } - private static MediaSourceFactory getMediaSourceFactory(String className) { - Constructor constructor = null; - Method setStreamKeysMethod = null; - Method createMethod = null; + @Nullable + private static Constructor getConstructor(String className) { try { // LINT.IfChange - Class factoryClazz = Class.forName(className); - constructor = factoryClazz.getConstructor(Factory.class); - setStreamKeysMethod = factoryClazz.getMethod("setStreamKeys", List.class); - createMethod = factoryClazz.getMethod("createMediaSource", Uri.class); + Class factoryClazz = + Class.forName(className).asSubclass(MediaSourceFactory.class); + return factoryClazz.getConstructor(Factory.class); // LINT.ThenChange(../../../../../../../../proguard-rules.txt) } catch (ClassNotFoundException e) { // Expected if the app was built without the respective module. - } catch (NoSuchMethodException | SecurityException e) { + return null; + } catch (NoSuchMethodException e) { // Something is wrong with the library or the proguard configuration. throw new IllegalStateException(e); } - return new MediaSourceFactory(constructor, setStreamKeysMethod, createMethod); } - private static final class MediaSourceFactory { - @Nullable private final Constructor constructor; - @Nullable private final Method setStreamKeysMethod; - @Nullable private final Method createMethod; - - public MediaSourceFactory( - @Nullable Constructor constructor, - @Nullable Method setStreamKeysMethod, - @Nullable Method createMethod) { - this.constructor = constructor; - this.setStreamKeysMethod = setStreamKeysMethod; - this.createMethod = createMethod; + private static MediaSource createMediaSourceInternal( + @Nullable Constructor constructor, + Uri uri, + Factory dataSourceFactory, + @Nullable DrmSessionManager drmSessionManager, + @Nullable List streamKeys) { + if (constructor == null) { + throw new IllegalStateException("Module missing to create media source."); } - - private MediaSource createMediaSource( - Uri uri, Factory dataSourceFactory, @Nullable List streamKeys) { - if (constructor == null || setStreamKeysMethod == null || createMethod == null) { - throw new IllegalStateException("Module missing to create media source."); + try { + MediaSourceFactory factory = constructor.newInstance(dataSourceFactory); + if (drmSessionManager != null) { + factory.setDrmSessionManager(drmSessionManager); } - try { - Object factory = constructor.newInstance(dataSourceFactory); - if (streamKeys != null) { - setStreamKeysMethod.invoke(factory, streamKeys); - } - return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri)); - } catch (Exception e) { - throw new IllegalStateException("Failed to instantiate media source.", e); + if (streamKeys != null) { + factory.setStreamKeys(streamKeys); } + return Assertions.checkNotNull(factory.createMediaSource(uri)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate media source.", e); } } private static final class MediaPreparer - implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback { + implements MediaSourceCaller, MediaPeriod.Callback, Handler.Callback { private static final int MESSAGE_PREPARE_SOURCE = 0; private static final int MESSAGE_CHECK_FOR_FAILURE = 1; @@ -822,7 +956,6 @@ public final class DownloadHelper { private final HandlerThread mediaSourceThread; private final Handler mediaSourceHandler; - @Nullable public Object manifest; public @MonotonicNonNull Timeline timeline; public MediaPeriod @MonotonicNonNull [] mediaPeriods; @@ -856,7 +989,7 @@ public final class DownloadHelper { public boolean handleMessage(Message msg) { switch (msg.what) { case MESSAGE_PREPARE_SOURCE: - mediaSource.prepareSource(/* listener= */ this, /* mediaTransferListener= */ null); + mediaSource.prepareSource(/* caller= */ this, /* mediaTransferListener= */ null); mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); return true; case MESSAGE_CHECK_FOR_FAILURE: @@ -897,17 +1030,23 @@ public final class DownloadHelper { } } - // MediaSource.SourceInfoRefreshListener implementation. + // MediaSource.MediaSourceCaller implementation. @Override - public void onSourceInfoRefreshed( - MediaSource source, Timeline timeline, @Nullable Object manifest) { + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline) { if (this.timeline != null) { // Ignore dynamic updates. return; } + if (timeline.getWindow(/* windowIndex= */ 0, new Timeline.Window()).isLive) { + downloadHelperHandler + .obtainMessage( + DOWNLOAD_HELPER_CALLBACK_MESSAGE_FAILED, + /* obj= */ new LiveContentUnsupportedException()) + .sendToTarget(); + return; + } this.timeline = timeline; - this.manifest = manifest; mediaPeriods = new MediaPeriod[timeline.getPeriodCount()]; for (int i = 0; i < mediaPeriods.length; i++) { MediaPeriod mediaPeriod = @@ -997,6 +1136,16 @@ public final class DownloadHelper { public Object getSelectionData() { return null; } + + @Override + public void updateSelectedTrack( + long playbackPositionUs, + long bufferedDurationUs, + long availableDurationUs, + List queue, + MediaChunkIterator[] mediaChunkIterators) { + // Do nothing. + } } private static final class DummyBandwidthMeter implements BandwidthMeter { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java index 3de1b7b21..e0ccd23c7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java @@ -16,14 +16,18 @@ package com.google.android.exoplayer2.offline; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import java.io.IOException; /** An index of {@link Download Downloads}. */ +@WorkerThread public interface DownloadIndex { /** * Returns the {@link Download} with the given {@code id}, or null. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param id ID of a {@link Download}. * @return The {@link Download} with the given {@code id}, or null if a download state with this * id doesn't exist. @@ -35,6 +39,8 @@ public interface DownloadIndex { /** * Returns a {@link DownloadCursor} to {@link Download}s with the given {@code states}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param states Returns only the {@link Download}s with this states. If empty, returns all. * @return A cursor to {@link Download}s with the given {@code states}. * @throws IOException If an error occurs reading the state. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index ec5ff81d9..66b2a7cf9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -75,6 +75,16 @@ public final class DownloadManager { */ default void onInitialized(DownloadManager downloadManager) {} + /** + * Called when downloads are ({@link #pauseDownloads() paused} or {@link #resumeDownloads() + * resumed}. + * + * @param downloadManager The reporting instance. + * @param downloadsPaused Whether downloads are currently paused. + */ + default void onDownloadsPausedChanged( + DownloadManager downloadManager, boolean downloadsPaused) {} + /** * Called when the state of a download changes. * @@ -110,6 +120,19 @@ public final class DownloadManager { DownloadManager downloadManager, Requirements requirements, @Requirements.RequirementFlags int notMetRequirements) {} + + /** + * Called when there is a change in whether this manager has one or more downloads that are not + * progressing for the sole reason that the {@link #getRequirements() Requirements} are not met. + * See {@link #isWaitingForRequirements()} for more information. + * + * @param downloadManager The reporting instance. + * @param waitingForRequirements Whether this manager has one or more downloads that are not + * progressing for the sole reason that the {@link #getRequirements() Requirements} are not + * met. + */ + default void onWaitingForRequirementsChanged( + DownloadManager downloadManager, boolean waitingForRequirements) {} } /** The default maximum number of parallel downloads. */ @@ -155,6 +178,7 @@ public final class DownloadManager { private int maxParallelDownloads; private int minRetryCount; private int notMetRequirements; + private boolean waitingForRequirements; private List downloads; private RequirementsWatcher requirementsWatcher; @@ -238,17 +262,16 @@ public final class DownloadManager { /** * Returns whether this manager has one or more downloads that are not progressing for the sole - * reason that the {@link #getRequirements() Requirements} are not met. + * reason that the {@link #getRequirements() Requirements} are not met. This is true if: + * + *

    + *
  • The {@link #getRequirements() Requirements} are not met. + *
  • The downloads are not paused (i.e. {@link #getDownloadsPaused()} is {@code false}). + *
  • There are downloads in the {@link Download#STATE_QUEUED queued state}. + *
*/ public boolean isWaitingForRequirements() { - if (!downloadsPaused && notMetRequirements != 0) { - for (int i = 0; i < downloads.size(); i++) { - if (downloads.get(i).state == STATE_QUEUED) { - return true; - } - } - } - return false; + return waitingForRequirements; } /** @@ -281,7 +304,7 @@ public final class DownloadManager { */ @Requirements.RequirementFlags public int getNotMetRequirements() { - return getRequirements().getNotMetRequirements(context); + return notMetRequirements; } /** @@ -374,29 +397,15 @@ public final class DownloadManager { * {@link Download#stopReason stopReasons}. */ public void resumeDownloads() { - if (!downloadsPaused) { - return; - } - downloadsPaused = false; - pendingMessages++; - internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 0, /* unused */ 0) - .sendToTarget(); + setDownloadsPaused(/* downloadsPaused= */ false); } /** - * Pauses downloads. Downloads that would otherwise be making progress transition to {@link + * Pauses downloads. Downloads that would otherwise be making progress will transition to {@link * Download#STATE_QUEUED}. */ public void pauseDownloads() { - if (downloadsPaused) { - return; - } - downloadsPaused = true; - pendingMessages++; - internalHandler - .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, /* downloadsPaused */ 1, /* unused */ 0) - .sendToTarget(); + setDownloadsPaused(/* downloadsPaused= */ true); } /** @@ -481,6 +490,26 @@ public final class DownloadManager { pendingMessages = 0; activeTaskCount = 0; initialized = false; + notMetRequirements = 0; + waitingForRequirements = false; + } + } + + private void setDownloadsPaused(boolean downloadsPaused) { + if (this.downloadsPaused == downloadsPaused) { + return; + } + this.downloadsPaused = downloadsPaused; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_DOWNLOADS_PAUSED, downloadsPaused ? 1 : 0, /* unused */ 0) + .sendToTarget(); + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); + for (Listener listener : listeners) { + listener.onDownloadsPausedChanged(this, downloadsPaused); + } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); } } @@ -488,17 +517,41 @@ public final class DownloadManager { RequirementsWatcher requirementsWatcher, @Requirements.RequirementFlags int notMetRequirements) { Requirements requirements = requirementsWatcher.getRequirements(); + if (this.notMetRequirements != notMetRequirements) { + this.notMetRequirements = notMetRequirements; + pendingMessages++; + internalHandler + .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0) + .sendToTarget(); + } + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); for (Listener listener : listeners) { listener.onRequirementsStateChanged(this, requirements, notMetRequirements); } - if (this.notMetRequirements == notMetRequirements) { - return; + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } + } + + private boolean updateWaitingForRequirements() { + boolean waitingForRequirements = false; + if (!downloadsPaused && notMetRequirements != 0) { + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == STATE_QUEUED) { + waitingForRequirements = true; + break; + } + } + } + boolean waitingForRequirementsChanged = this.waitingForRequirements != waitingForRequirements; + this.waitingForRequirements = waitingForRequirements; + return waitingForRequirementsChanged; + } + + private void notifyWaitingForRequirementsChanged() { + for (Listener listener : listeners) { + listener.onWaitingForRequirementsChanged(this, waitingForRequirements); } - this.notMetRequirements = notMetRequirements; - pendingMessages++; - internalHandler - .obtainMessage(MSG_SET_NOT_MET_REQUIREMENTS, notMetRequirements, /* unused */ 0) - .sendToTarget(); } // Main thread message handling. @@ -528,14 +581,19 @@ public final class DownloadManager { private void onInitialized(List downloads) { initialized = true; this.downloads = Collections.unmodifiableList(downloads); + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); for (Listener listener : listeners) { listener.onInitialized(DownloadManager.this); } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } } private void onDownloadUpdate(DownloadUpdate update) { downloads = Collections.unmodifiableList(update.downloads); Download updatedDownload = update.download; + boolean waitingForRequirementsChanged = updateWaitingForRequirements(); if (update.isRemove) { for (Listener listener : listeners) { listener.onDownloadRemoved(this, updatedDownload); @@ -545,6 +603,9 @@ public final class DownloadManager { listener.onDownloadChanged(this, updatedDownload); } } + if (waitingForRequirementsChanged) { + notifyWaitingForRequirementsChanged(); + } } private void onMessageProcessed(int processedMessageCount, int activeTaskCount) { @@ -731,7 +792,7 @@ public final class DownloadManager { Log.e(TAG, "Failed to set manual stop reason", e); } } else { - Download download = getDownload(id, /* loadFromIndex= */ false); + @Nullable Download download = getDownload(id, /* loadFromIndex= */ false); if (download != null) { setStopReason(download, stopReason); } else { @@ -779,7 +840,7 @@ public final class DownloadManager { } private void addDownload(DownloadRequest request, int stopReason) { - Download download = getDownload(request.id, /* loadFromIndex= */ true); + @Nullable Download download = getDownload(request.id, /* loadFromIndex= */ true); long nowMs = System.currentTimeMillis(); if (download != null) { putDownload(mergeRequest(download, request, stopReason, nowMs)); @@ -798,7 +859,7 @@ public final class DownloadManager { } private void removeDownload(String id) { - Download download = getDownload(id, /* loadFromIndex= */ true); + @Nullable Download download = getDownload(id, /* loadFromIndex= */ true); if (download == null) { Log.e(TAG, "Failed to remove nonexistent download: " + id); return; @@ -860,7 +921,7 @@ public final class DownloadManager { int accumulatingDownloadTaskCount = 0; for (int i = 0; i < downloads.size(); i++) { Download download = downloads.get(i); - Task activeTask = activeTasks.get(download.request.id); + @Nullable Task activeTask = activeTasks.get(download.request.id); switch (download.state) { case STATE_STOPPED: syncStoppedDownload(activeTask); @@ -999,7 +1060,7 @@ public final class DownloadManager { return; } - Throwable finalError = task.finalError; + @Nullable Throwable finalError = task.finalError; if (finalError != null) { Log.e(TAG, "Task failed: " + task.request + ", " + isRemove, finalError); } @@ -1176,7 +1237,7 @@ public final class DownloadManager { private final boolean isRemove; private final int minRetryCount; - private volatile InternalHandler internalHandler; + @Nullable private volatile InternalHandler internalHandler; private volatile boolean isCanceled; @Nullable private Throwable finalError; @@ -1246,7 +1307,7 @@ public final class DownloadManager { } catch (Throwable e) { finalError = e; } - Handler internalHandler = this.internalHandler; + @Nullable Handler internalHandler = this.internalHandler; if (internalHandler != null) { internalHandler.obtainMessage(MSG_TASK_STOPPED, this).sendToTarget(); } @@ -1258,7 +1319,7 @@ public final class DownloadManager { downloadProgress.percentDownloaded = percentDownloaded; if (contentLength != this.contentLength) { this.contentLength = contentLength; - Handler internalHandler = this.internalHandler; + @Nullable Handler internalHandler = this.internalHandler; if (internalHandler != null) { internalHandler.obtainMessage(MSG_CONTENT_LENGTH_CHANGED, this).sendToTarget(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java index 7ff43ceac..988b90814 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadRequest.java @@ -100,8 +100,7 @@ public final class DownloadRequest implements Parcelable { } streamKeys = Collections.unmodifiableList(mutableStreamKeys); customCacheKey = in.readString(); - data = new byte[in.readInt()]; - in.readByteArray(data); + data = castNonNull(in.createByteArray()); } /** @@ -194,7 +193,6 @@ public final class DownloadRequest implements Parcelable { dest.writeParcelable(streamKeys.get(i), /* parcelableFlags= */ 0); } dest.writeString(customCacheKey); - dest.writeInt(data.length); dest.writeByteArray(data); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 6587984f0..f78e9bb54 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.Util; import java.util.HashMap; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A {@link Service} for downloading media. */ public abstract class DownloadService extends Service { @@ -166,20 +167,22 @@ public abstract class DownloadService extends Service { private static final String TAG = "DownloadService"; - // Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the - // process is running). This allows DownloadService to restart when there's no scheduler. + // Keep a DownloadManagerHelper for each DownloadService as long as the process is running. The + // helper is needed to restart the DownloadService when there's no scheduler. Even when there is a + // scheduler, the DownloadManagerHelper is typically able to restart the DownloadService faster. private static final HashMap, DownloadManagerHelper> - downloadManagerListeners = new HashMap<>(); + downloadManagerHelpers = new HashMap<>(); @Nullable private final ForegroundNotificationUpdater foregroundNotificationUpdater; @Nullable private final String channelId; @StringRes private final int channelNameResourceId; @StringRes private final int channelDescriptionResourceId; - private DownloadManager downloadManager; + @MonotonicNonNull private DownloadManager downloadManager; private int lastStartId; private boolean startedInForeground; private boolean taskRemoved; + private boolean isStopped; private boolean isDestroyed; /** @@ -575,42 +578,48 @@ public abstract class DownloadService extends Service { NotificationUtil.IMPORTANCE_LOW); } Class clazz = getClass(); - DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); + @Nullable DownloadManagerHelper downloadManagerHelper = downloadManagerHelpers.get(clazz); if (downloadManagerHelper == null) { - DownloadManager downloadManager = getDownloadManager(); + boolean foregroundAllowed = foregroundNotificationUpdater != null; + @Nullable Scheduler scheduler = foregroundAllowed ? getScheduler() : null; + downloadManager = getDownloadManager(); downloadManager.resumeDownloads(); downloadManagerHelper = new DownloadManagerHelper( - getApplicationContext(), downloadManager, getScheduler(), clazz); - downloadManagerListeners.put(clazz, downloadManagerHelper); + getApplicationContext(), downloadManager, foregroundAllowed, scheduler, clazz); + downloadManagerHelpers.put(clazz, downloadManagerHelper); + } else { + downloadManager = downloadManagerHelper.downloadManager; } - downloadManager = downloadManagerHelper.downloadManager; downloadManagerHelper.attachService(this); } @Override - public int onStartCommand(Intent intent, int flags, int startId) { + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { lastStartId = startId; taskRemoved = false; - String intentAction = null; - String contentId = null; + @Nullable String intentAction = null; + @Nullable String contentId = null; if (intent != null) { intentAction = intent.getAction(); + contentId = intent.getStringExtra(KEY_CONTENT_ID); startedInForeground |= intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); - contentId = intent.getStringExtra(KEY_CONTENT_ID); } // intentAction is null if the service is restarted or no action is specified. if (intentAction == null) { intentAction = ACTION_INIT; } + DownloadManager downloadManager = Assertions.checkNotNull(this.downloadManager); switch (intentAction) { case ACTION_INIT: case ACTION_RESTART: // Do nothing. break; case ACTION_ADD_DOWNLOAD: - DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); + @Nullable + DownloadRequest downloadRequest = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_DOWNLOAD_REQUEST); if (downloadRequest == null) { Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { @@ -635,7 +644,7 @@ public abstract class DownloadService extends Service { downloadManager.pauseDownloads(); break; case ACTION_SET_STOP_REASON: - if (!intent.hasExtra(KEY_STOP_REASON)) { + if (!Assertions.checkNotNull(intent).hasExtra(KEY_STOP_REASON)) { Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); } else { int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); @@ -643,7 +652,9 @@ public abstract class DownloadService extends Service { } break; case ACTION_SET_REQUIREMENTS: - Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); + @Nullable + Requirements requirements = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_REQUIREMENTS); if (requirements == null) { Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { @@ -655,6 +666,12 @@ public abstract class DownloadService extends Service { break; } + if (Util.SDK_INT >= 26 && startedInForeground && foregroundNotificationUpdater != null) { + // From API level 26, services started in the foreground are required to show a notification. + foregroundNotificationUpdater.showNotificationIfNotAlready(); + } + + isStopped = false; if (downloadManager.isIdle()) { stop(); } @@ -669,9 +686,9 @@ public abstract class DownloadService extends Service { @Override public void onDestroy() { isDestroyed = true; - DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass()); - boolean unschedule = !downloadManager.isWaitingForRequirements(); - downloadManagerHelper.detachService(this, unschedule); + DownloadManagerHelper downloadManagerHelper = + Assertions.checkNotNull(downloadManagerHelpers.get(getClass())); + downloadManagerHelper.detachService(this); if (foregroundNotificationUpdater != null) { foregroundNotificationUpdater.stopPeriodicUpdates(); } @@ -696,21 +713,21 @@ public abstract class DownloadService extends Service { * Returns a {@link Scheduler} to restart the service when requirements allowing downloads to take * place are met. If {@code null}, the service will only be restarted if the process is still in * memory when the requirements are met. + * + *

This method is not called for services whose {@code foregroundNotificationId} is set to + * {@link #FOREGROUND_NOTIFICATION_ID_NONE}. Such services will only be restarted if the process + * is still in memory and considered non-idle, meaning that it's either in the foreground or was + * backgrounded within the last few minutes. */ - protected abstract @Nullable Scheduler getScheduler(); + @Nullable + protected abstract Scheduler getScheduler(); /** - * Returns a notification to be displayed when this service running in the foreground. This method - * is called when there is a download state change and periodically while there are active - * downloads. The periodic update interval can be set using {@link #DownloadService(int, long)}. - * - *

On API level 26 and above, this method may also be called just before the service stops, - * with an empty {@code downloads} array. The returned notification is used to satisfy system - * requirements for foreground services. + * Returns a notification to be displayed when this service running in the foreground. * *

Download services that do not wish to run in the foreground should be created by setting the * {@code foregroundNotificationId} constructor argument to {@link - * #FOREGROUND_NOTIFICATION_ID_NONE}. This method will not be called in this case, meaning it can + * #FOREGROUND_NOTIFICATION_ID_NONE}. This method is not called for such services, meaning it can * be implemented to throw {@link UnsupportedOperationException}. * * @param downloads The current downloads. @@ -729,29 +746,52 @@ public abstract class DownloadService extends Service { } /** - * Called when the state of a download changes. The default implementation is a no-op. - * - * @param download The new state of the download. + * @deprecated Some state change events may not be delivered to this method. Instead, use {@link + * DownloadManager#addListener(DownloadManager.Listener)} to register a listener directly to + * the {@link DownloadManager} that you return through {@link #getDownloadManager()}. */ + @Deprecated protected void onDownloadChanged(Download download) { // Do nothing. } /** - * Called when a download is removed. The default implementation is a no-op. - * - * @param download The last state of the download before it was removed. + * @deprecated Some download removal events may not be delivered to this method. Instead, use + * {@link DownloadManager#addListener(DownloadManager.Listener)} to register a listener + * directly to the {@link DownloadManager} that you return through {@link + * #getDownloadManager()}. */ + @Deprecated protected void onDownloadRemoved(Download download) { // Do nothing. } + /** + * Called after the service is created, once the downloads are known. + * + * @param downloads The current downloads. + */ + private void notifyDownloads(List downloads) { + if (foregroundNotificationUpdater != null) { + for (int i = 0; i < downloads.size(); i++) { + if (needsStartedService(downloads.get(i).state)) { + foregroundNotificationUpdater.startPeriodicUpdates(); + break; + } + } + } + } + + /** + * Called when the state of a download changes. + * + * @param download The state of the download. + */ + @SuppressWarnings("deprecation") private void notifyDownloadChanged(Download download) { onDownloadChanged(download); if (foregroundNotificationUpdater != null) { - if (download.state == Download.STATE_DOWNLOADING - || download.state == Download.STATE_REMOVING - || download.state == Download.STATE_RESTARTING) { + if (needsStartedService(download.state)) { foregroundNotificationUpdater.startPeriodicUpdates(); } else { foregroundNotificationUpdater.invalidate(); @@ -759,6 +799,12 @@ public abstract class DownloadService extends Service { } } + /** + * Called when a download is removed. + * + * @param download The last state of the download before it was removed. + */ + @SuppressWarnings("deprecation") private void notifyDownloadRemoved(Download download) { onDownloadRemoved(download); if (foregroundNotificationUpdater != null) { @@ -766,21 +812,29 @@ public abstract class DownloadService extends Service { } } + /** Returns whether the service is stopped. */ + private boolean isStopped() { + return isStopped; + } + private void stop() { if (foregroundNotificationUpdater != null) { foregroundNotificationUpdater.stopPeriodicUpdates(); - // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260]. - if (startedInForeground && Util.SDK_INT >= 26) { - foregroundNotificationUpdater.showNotificationIfNotAlready(); - } } if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. stopSelf(); + isStopped = true; } else { - stopSelfResult(lastStartId); + isStopped |= stopSelfResult(lastStartId); } } + private static boolean needsStartedService(@Download.State int state) { + return state == Download.STATE_DOWNLOADING + || state == Download.STATE_REMOVING + || state == Download.STATE_RESTARTING; + } + private static Intent getIntent( Context context, Class clazz, String action, boolean foreground) { return getIntent(context, clazz, action).putExtra(KEY_FOREGROUND, foreground); @@ -804,7 +858,6 @@ public abstract class DownloadService extends Service { private final int notificationId; private final long updateInterval; private final Handler handler; - private final Runnable updateRunnable; private boolean periodicUpdatesStarted; private boolean notificationDisplayed; @@ -813,7 +866,6 @@ public abstract class DownloadService extends Service { this.notificationId = notificationId; this.updateInterval = updateInterval; this.handler = new Handler(Looper.getMainLooper()); - this.updateRunnable = this::update; } public void startPeriodicUpdates() { @@ -823,7 +875,7 @@ public abstract class DownloadService extends Service { public void stopPeriodicUpdates() { periodicUpdatesStarted = false; - handler.removeCallbacks(updateRunnable); + handler.removeCallbacksAndMessages(null); } public void showNotificationIfNotAlready() { @@ -839,12 +891,12 @@ public abstract class DownloadService extends Service { } private void update() { - List downloads = downloadManager.getCurrentDownloads(); + List downloads = Assertions.checkNotNull(downloadManager).getCurrentDownloads(); startForeground(notificationId, getForegroundNotification(downloads)); notificationDisplayed = true; if (periodicUpdatesStarted) { - handler.removeCallbacks(updateRunnable); - handler.postDelayed(updateRunnable, updateInterval); + handler.removeCallbacksAndMessages(null); + handler.postDelayed(this::update, updateInterval); } } } @@ -853,6 +905,7 @@ public abstract class DownloadService extends Service { private final Context context; private final DownloadManager downloadManager; + private final boolean foregroundAllowed; @Nullable private final Scheduler scheduler; private final Class serviceClass; @Nullable private DownloadService downloadService; @@ -860,37 +913,63 @@ public abstract class DownloadService extends Service { private DownloadManagerHelper( Context context, DownloadManager downloadManager, + boolean foregroundAllowed, @Nullable Scheduler scheduler, Class serviceClass) { this.context = context; this.downloadManager = downloadManager; + this.foregroundAllowed = foregroundAllowed; this.scheduler = scheduler; this.serviceClass = serviceClass; downloadManager.addListener(this); - if (scheduler != null) { - Requirements requirements = downloadManager.getRequirements(); - setSchedulerEnabled(/* enabled= */ !requirements.checkRequirements(context), requirements); - } + updateScheduler(); } public void attachService(DownloadService downloadService) { Assertions.checkState(this.downloadService == null); this.downloadService = downloadService; + if (downloadManager.isInitialized()) { + // The call to DownloadService.notifyDownloads is posted to avoid it being called directly + // from DownloadService.onCreate. This is a good idea because it may in turn call + // DownloadService.getForegroundNotification, and concrete subclass implementations may + // not anticipate the possibility of this method being called before their onCreate + // implementation has finished executing. + new Handler() + .postAtFrontOfQueue( + () -> downloadService.notifyDownloads(downloadManager.getCurrentDownloads())); + } } - public void detachService(DownloadService downloadService, boolean unschedule) { + public void detachService(DownloadService downloadService) { Assertions.checkState(this.downloadService == downloadService); this.downloadService = null; - if (scheduler != null && unschedule) { + if (scheduler != null && !downloadManager.isWaitingForRequirements()) { scheduler.cancel(); } } + // DownloadManager.Listener implementation. + + @Override + public void onInitialized(DownloadManager downloadManager) { + if (downloadService != null) { + downloadService.notifyDownloads(downloadManager.getCurrentDownloads()); + } + } + @Override public void onDownloadChanged(DownloadManager downloadManager, Download download) { if (downloadService != null) { downloadService.notifyDownloadChanged(download); } + if (serviceMayNeedRestart() && needsStartedService(download.state)) { + // This shouldn't happen unless (a) application code is changing the downloads by calling + // the DownloadManager directly rather than sending actions through the service, or (b) if + // the service is background only and a previous attempt to start it was prevented. Try and + // restart the service to robust against such cases. + Log.w(TAG, "DownloadService wasn't running. Restarting."); + restartService(); + } } @Override @@ -908,34 +987,62 @@ public abstract class DownloadService extends Service { } @Override - public void onRequirementsStateChanged( - DownloadManager downloadManager, - Requirements requirements, - @Requirements.RequirementFlags int notMetRequirements) { - boolean requirementsMet = notMetRequirements == 0; - if (downloadService == null && requirementsMet) { + public void onWaitingForRequirementsChanged( + DownloadManager downloadManager, boolean waitingForRequirements) { + if (!waitingForRequirements + && !downloadManager.getDownloadsPaused() + && serviceMayNeedRestart()) { + // We're no longer waiting for requirements and downloads aren't paused, meaning the manager + // will be able to resume downloads that are currently queued. If there exist queued + // downloads then we should ensure the service is started. + List downloads = downloadManager.getCurrentDownloads(); + for (int i = 0; i < downloads.size(); i++) { + if (downloads.get(i).state == Download.STATE_QUEUED) { + restartService(); + break; + } + } + } + updateScheduler(); + } + + // Internal methods. + + private boolean serviceMayNeedRestart() { + return downloadService == null || downloadService.isStopped(); + } + + private void restartService() { + if (foregroundAllowed) { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_RESTART); + Util.startForegroundService(context, intent); + } else { + // The service is background only. Use ACTION_INIT rather than ACTION_RESTART because + // ACTION_RESTART is handled as though KEY_FOREGROUND is set to true. try { Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); context.startService(intent); } catch (IllegalStateException e) { - /* startService fails if the app is in the background then don't stop the scheduler. */ - return; + // The process is classed as idle by the platform. Starting a background service is not + // allowed in this state. + Log.w(TAG, "Failed to restart DownloadService (process is idle)."); } } - if (scheduler != null) { - setSchedulerEnabled(/* enabled= */ !requirementsMet, requirements); - } } - private void setSchedulerEnabled(boolean enabled, Requirements requirements) { - if (!enabled) { - scheduler.cancel(); - } else { + private void updateScheduler() { + if (scheduler == null) { + return; + } + if (downloadManager.isWaitingForRequirements()) { String servicePackage = context.getPackageName(); + Requirements requirements = downloadManager.getRequirements(); boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); if (!success) { Log.e(TAG, "Scheduling downloads failed."); } + } else { + scheduler.cancel(); } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java index 65dcd187a..0d53b3cde 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -20,7 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.FileDataSourceFactory; +import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.PriorityDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSink; @@ -59,7 +59,8 @@ public final class DownloaderConstructorHelper { * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for * downloading data. * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s - * for reading data from the cache. If null then a {@link FileDataSourceFactory} will be used. + * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be + * used. * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used. * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, @@ -86,7 +87,8 @@ public final class DownloaderConstructorHelper { * @param upstreamFactory A {@link DataSource.Factory} for creating {@link DataSource}s for * downloading data. * @param cacheReadDataSourceFactory A {@link DataSource.Factory} for creating {@link DataSource}s - * for reading data from the cache. If null then a {@link FileDataSourceFactory} will be used. + * for reading data from the cache. If null then a {@link FileDataSource.Factory} will be + * used. * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for creating {@link DataSource}s * for writing data to the cache. If null then a {@link CacheDataSinkFactory} will be used. * @param priorityTaskManager A {@link PriorityTaskManager} to use when downloading. If non-null, @@ -108,7 +110,7 @@ public final class DownloaderConstructorHelper { DataSource.Factory readDataSourceFactory = cacheReadDataSourceFactory != null ? cacheReadDataSourceFactory - : new FileDataSourceFactory(); + : new FileDataSource.Factory(); if (cacheWriteDataSinkFactory == null) { cacheWriteDataSinkFactory = new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index 17f4047bc..a73258272 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -29,6 +29,12 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * A downloader for progressive media streams. + * + *

The downloader attempts to download the entire media bytes referenced by a {@link Uri} into a + * cache as defined by {@link DownloaderConstructorHelper}. Callers can use the constructor to + * specify a custom cache key for the downloaded bytes. + * + *

The downloader will avoid downloading already-downloaded media bytes. */ public final class ProgressiveDownloader implements Downloader { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 1643812ec..969003101 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -16,9 +16,8 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -57,7 +56,7 @@ public abstract class SegmentDownloader> impleme } @Override - public int compareTo(@NonNull Segment other) { + public int compareTo(Segment other) { return Util.compareLong(startTimeUs, other.startTimeUs); } } @@ -138,7 +137,7 @@ public abstract class SegmentDownloader> impleme Collections.sort(segments); // Download the segments. - ProgressNotifier progressNotifier = null; + @Nullable ProgressNotifier progressNotifier = null; if (progressListener != null) { progressNotifier = new ProgressNotifier( diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java index 977be9a19..f9a48868d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/StreamKey.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.offline; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; /** @@ -92,7 +91,7 @@ public final class StreamKey implements Comparable, Parcelable { // Comparable implementation. @Override - public int compareTo(@NonNull StreamKey o) { + public int compareTo(StreamKey o) { int result = periodIndex - o.periodIndex; if (result == 0) { result = groupIndex - o.groupIndex; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java index dc7085c85..d49b4c39c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/WritableDownloadIndex.java @@ -15,14 +15,18 @@ */ package com.google.android.exoplayer2.offline; +import androidx.annotation.WorkerThread; import java.io.IOException; /** A writable index of {@link Download Downloads}. */ +@WorkerThread public interface WritableDownloadIndex extends DownloadIndex { /** * Adds or replaces a {@link Download}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param download The {@link Download} to be added. * @throws IOException If an error occurs setting the state. */ @@ -32,6 +36,8 @@ public interface WritableDownloadIndex extends DownloadIndex { * Removes the download with the given ID. Does nothing if a download with the given ID does not * exist. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param id The ID of the download to remove. * @throws IOException If an error occurs removing the state. */ @@ -40,6 +46,8 @@ public interface WritableDownloadIndex extends DownloadIndex { /** * Sets all {@link Download#STATE_DOWNLOADING} states to {@link Download#STATE_QUEUED}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @throws IOException If an error occurs updating the state. */ void setDownloadingStatesToQueued() throws IOException; @@ -47,6 +55,8 @@ public interface WritableDownloadIndex extends DownloadIndex { /** * Sets all states to {@link Download#STATE_REMOVING}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @throws IOException If an error occurs updating the state. */ void setStatesToRemoving() throws IOException; @@ -55,6 +65,8 @@ public interface WritableDownloadIndex extends DownloadIndex { * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED}, * {@link Download#STATE_FAILED}). * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param stopReason The stop reason. * @throws IOException If an error occurs updating the state. */ @@ -65,6 +77,8 @@ public interface WritableDownloadIndex extends DownloadIndex { * Download#STATE_COMPLETED}, {@link Download#STATE_FAILED}). Does nothing if a download with the * given ID does not exist, or if it's not in a terminal state. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param id The ID of the download to update. * @param stopReason The stop reason. * @throws IOException If an error occurs updating the state. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/package-info.java new file mode 100644 index 000000000..61450c9cf --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/package-info.java new file mode 100644 index 000000000..690f2c40c --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index 752239c99..fcebc9388 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -64,6 +64,7 @@ public final class PlatformScheduler implements Scheduler { */ @RequiresPermission(android.Manifest.permission.RECEIVE_BOOT_COMPLETED) public PlatformScheduler(Context context, int jobId) { + context = context.getApplicationContext(); this.jobId = jobId; jobServiceComponentName = new ComponentName(context, PlatformSchedulerService.class); jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 5db86935f..4e2c83d5d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -27,6 +27,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PowerManager; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; @@ -128,8 +129,9 @@ public final class Requirements implements Parcelable { } ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = Assertions.checkNotNull(connectivityManager).getActiveNetworkInfo(); + (ConnectivityManager) + Assertions.checkNotNull(context.getSystemService(Context.CONNECTIVITY_SERVICE)); + @Nullable NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); if (networkInfo == null || !networkInfo.isConnected() || !isInternetConnectivityValidated(connectivityManager)) { @@ -155,32 +157,35 @@ public final class Requirements implements Parcelable { } private boolean isDeviceIdle(Context context) { - PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + PowerManager powerManager = + (PowerManager) Assertions.checkNotNull(context.getSystemService(Context.POWER_SERVICE)); return Util.SDK_INT >= 23 ? powerManager.isDeviceIdleMode() : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn(); } private static boolean isInternetConnectivityValidated(ConnectivityManager connectivityManager) { - if (Util.SDK_INT < 23) { - // TODO Check internet connectivity using http://clients3.google.com/generate_204 on API - // levels prior to 23. + // It's possible to check NetworkCapabilities.NET_CAPABILITY_VALIDATED from API level 23, but + // RequirementsWatcher only fires an event to re-check the requirements when NetworkCapabilities + // change from API level 24. We assume that network capability is validated for API level 23 to + // keep in sync. + if (Util.SDK_INT < 24) { return true; } - Network activeNetwork = connectivityManager.getActiveNetwork(); + + @Nullable Network activeNetwork = connectivityManager.getActiveNetwork(); if (activeNetwork == null) { return false; } + @Nullable NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork); - boolean validated = - networkCapabilities == null - || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - return !validated; + return networkCapabilities != null + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index 9bd550f86..80015cf3a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -23,10 +23,10 @@ import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; -import android.net.NetworkRequest; import android.os.Handler; import android.os.Looper; import android.os.PowerManager; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -58,10 +58,10 @@ public final class RequirementsWatcher { private final Requirements requirements; private final Handler handler; - private DeviceStatusChangeReceiver receiver; + @Nullable private DeviceStatusChangeReceiver receiver; @Requirements.RequirementFlags private int notMetRequirements; - private CapabilityValidatedCallback networkCallback; + @Nullable private NetworkCallback networkCallback; /** * @param context Any context. @@ -87,8 +87,8 @@ public final class RequirementsWatcher { IntentFilter filter = new IntentFilter(); if (requirements.isNetworkRequired()) { - if (Util.SDK_INT >= 23) { - registerNetworkCallbackV23(); + if (Util.SDK_INT >= 24) { + registerNetworkCallbackV24(); } else { filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); } @@ -112,10 +112,10 @@ public final class RequirementsWatcher { /** Stops watching for changes. */ public void stop() { - context.unregisterReceiver(receiver); + context.unregisterReceiver(Assertions.checkNotNull(receiver)); receiver = null; - if (networkCallback != null) { - unregisterNetworkCallback(); + if (Util.SDK_INT >= 24 && networkCallback != null) { + unregisterNetworkCallbackV24(); } } @@ -124,26 +124,21 @@ public final class RequirementsWatcher { return requirements; } - @TargetApi(23) - private void registerNetworkCallbackV23() { + @TargetApi(24) + private void registerNetworkCallbackV24() { ConnectivityManager connectivityManager = Assertions.checkNotNull( (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); - NetworkRequest request = - new NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build(); - networkCallback = new CapabilityValidatedCallback(); - connectivityManager.registerNetworkCallback(request, networkCallback); + networkCallback = new NetworkCallback(); + connectivityManager.registerDefaultNetworkCallback(networkCallback); } - private void unregisterNetworkCallback() { - if (Util.SDK_INT >= 21) { - ConnectivityManager connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - connectivityManager.unregisterNetworkCallback(networkCallback); - networkCallback = null; - } + @TargetApi(24) + private void unregisterNetworkCallbackV24() { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.unregisterNetworkCallback(Assertions.checkNotNull(networkCallback)); + networkCallback = null; } private void checkRequirements() { @@ -155,6 +150,23 @@ public final class RequirementsWatcher { } } + /** + * Re-checks the requirements if there are network requirements that are currently not met. + * + *

When we receive an event that implies newly established network connectivity, we re-check + * the requirements by calling {@link #checkRequirements()}. This check sometimes sees that there + * is still no active network, meaning that any network requirements will remain not met. By + * calling this method when we receive other events that imply continued network connectivity, we + * can detect that the requirements are met once an active network does exist. + */ + private void recheckNotMetNetworkRequirements() { + if ((notMetRequirements & (Requirements.NETWORK | Requirements.NETWORK_UNMETERED)) == 0) { + // No unmet network requirements to recheck. + return; + } + checkRequirements(); + } + private class DeviceStatusChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -164,19 +176,43 @@ public final class RequirementsWatcher { } } - @RequiresApi(api = 21) - private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback { + @RequiresApi(24) + private final class NetworkCallback extends ConnectivityManager.NetworkCallback { + + private boolean receivedCapabilitiesChange; + private boolean networkValidated; + @Override public void onAvailable(Network network) { - onNetworkCallback(); + postCheckRequirements(); } @Override public void onLost(Network network) { - onNetworkCallback(); + postCheckRequirements(); } - private void onNetworkCallback() { + @Override + public void onBlockedStatusChanged(Network network, boolean blocked) { + if (!blocked) { + postRecheckNotMetNetworkRequirements(); + } + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + boolean networkValidated = + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + if (!receivedCapabilitiesChange || this.networkValidated != networkValidated) { + receivedCapabilitiesChange = true; + this.networkValidated = networkValidated; + postCheckRequirements(); + } else if (networkValidated) { + postRecheckNotMetNetworkRequirements(); + } + } + + private void postCheckRequirements() { handler.post( () -> { if (networkCallback != null) { @@ -184,5 +220,14 @@ public final class RequirementsWatcher { } }); } + + private void postRecheckNotMetNetworkRequirements() { + handler.post( + () -> { + if (networkCallback != null) { + recheckNotMetNetworkRequirements(); + } + }); + } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/package-info.java new file mode 100644 index 000000000..6273f325c --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/scheduler/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.scheduler; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 4a3505749..29ef1faa8 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -19,10 +19,9 @@ import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; -/** - * Abstract base class for the concatenation of one or more {@link Timeline}s. - */ +/** Abstract base class for the concatenation of one or more {@link Timeline}s. */ /* package */ abstract class AbstractConcatenatedTimeline extends Timeline { private final int childCount; @@ -35,6 +34,7 @@ import com.google.android.exoplayer2.Timeline; * @param concatenatedUid UID of a period in a concatenated timeline. * @return UID of the child timeline this period belongs to. */ + @SuppressWarnings("nullness:return.type.incompatible") public static Object getChildTimelineUidFromConcatenatedUid(Object concatenatedUid) { return ((Pair) concatenatedUid).first; } @@ -45,19 +45,20 @@ import com.google.android.exoplayer2.Timeline; * @param concatenatedUid UID of a period in a concatenated timeline. * @return UID of the period in the child timeline. */ + @SuppressWarnings("nullness:return.type.incompatible") public static Object getChildPeriodUidFromConcatenatedUid(Object concatenatedUid) { return ((Pair) concatenatedUid).second; } /** - * Returns concatenated UID for a period in a child timeline. + * Returns a concatenated UID for a period or window in a child timeline. * - * @param childTimelineUid UID of the child timeline this period belongs to. - * @param childPeriodUid UID of the period in the child timeline. - * @return UID of the period in the concatenated timeline. + * @param childTimelineUid UID of the child timeline this period or window belongs to. + * @param childPeriodOrWindowUid UID of the period or window in the child timeline. + * @return UID of the period or window in the concatenated timeline. */ - public static Object getConcatenatedUid(Object childTimelineUid, Object childPeriodUid) { - return Pair.create(childTimelineUid, childPeriodUid); + public static Object getConcatenatedUid(Object childTimelineUid, Object childPeriodOrWindowUid) { + return Pair.create(childTimelineUid, childPeriodOrWindowUid); } /** @@ -75,8 +76,8 @@ import com.google.android.exoplayer2.Timeline; } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled) { + public int getNextWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { if (isAtomic) { // Adapt repeat and shuffle mode to atomic concatenation. repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; @@ -85,10 +86,12 @@ import com.google.android.exoplayer2.Timeline; // Find next window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); - int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( - windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, - shuffleModeEnabled); + int nextWindowIndexInChild = + getTimelineByChildIndex(childIndex) + .getNextWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } @@ -109,8 +112,8 @@ import com.google.android.exoplayer2.Timeline; } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled) { + public int getPreviousWindowIndex( + int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { if (isAtomic) { // Adapt repeat and shuffle mode to atomic concatenation. repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode; @@ -119,10 +122,12 @@ import com.google.android.exoplayer2.Timeline; // Find previous window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); - int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( - windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, - shuffleModeEnabled); + int previousWindowIndexInChild = + getTimelineByChildIndex(childIndex) + .getPreviousWindowIndex( + windowIndex - firstWindowIndexInChild, + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } @@ -186,14 +191,18 @@ import com.google.android.exoplayer2.Timeline; } @Override - public final Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public final Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); getTimelineByChildIndex(childIndex) - .getWindow( - windowIndex - firstWindowIndexInChild, window, setTag, defaultPositionProjectionUs); + .getWindow(windowIndex - firstWindowIndexInChild, window, defaultPositionProjectionUs); + Object childUid = getChildUidByChildIndex(childIndex); + // Don't create new objects if the child is using SINGLE_WINDOW_UID. + window.uid = + Window.SINGLE_WINDOW_UID.equals(window.uid) + ? childUid + : getConcatenatedUid(childUid, window.uid); window.firstPeriodIndex += firstPeriodIndexInChild; window.lastPeriodIndex += firstPeriodIndexInChild; return window; @@ -216,11 +225,13 @@ import com.google.android.exoplayer2.Timeline; int childIndex = getChildIndexByPeriodIndex(periodIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); - getTimelineByChildIndex(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, - setIds); + getTimelineByChildIndex(childIndex) + .getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); period.windowIndex += firstWindowIndexInChild; if (setIds) { - period.uid = getConcatenatedUid(getChildUidByChildIndex(childIndex), period.uid); + period.uid = + getConcatenatedUid( + getChildUidByChildIndex(childIndex), Assertions.checkNotNull(period.uid)); } return period; } @@ -237,7 +248,8 @@ import com.google.android.exoplayer2.Timeline; return C.INDEX_UNSET; } int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid); - return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET + return periodIndexInChild == C.INDEX_UNSET + ? C.INDEX_UNSET : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild; } @@ -302,13 +314,14 @@ import com.google.android.exoplayer2.Timeline; protected abstract Object getChildUidByChildIndex(int childIndex); private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) + return shuffleModeEnabled + ? shuffleOrder.getNextIndex(childIndex) : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET; } private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) { - return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) + return shuffleModeEnabled + ? shuffleOrder.getPreviousIndex(childIndex) : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET; } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java index f6ea3da08..86e00e0a3 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -22,30 +22,33 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayList; +import java.util.HashSet; /** * Base {@link MediaSource} implementation to handle parallel reuse and to keep a list of {@link * MediaSourceEventListener}s. * - *

Whenever an implementing subclass needs to provide a new timeline and/or manifest, it must - * call {@link #refreshSourceInfo(Timeline, Object)} to notify all listeners. + *

Whenever an implementing subclass needs to provide a new timeline, it must call {@link + * #refreshSourceInfo(Timeline)} to notify all listeners. */ public abstract class BaseMediaSource implements MediaSource { - private final ArrayList sourceInfoListeners; + private final ArrayList mediaSourceCallers; + private final HashSet enabledMediaSourceCallers; private final MediaSourceEventListener.EventDispatcher eventDispatcher; @Nullable private Looper looper; @Nullable private Timeline timeline; - @Nullable private Object manifest; public BaseMediaSource() { - sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); + mediaSourceCallers = new ArrayList<>(/* initialCapacity= */ 1); + enabledMediaSourceCallers = new HashSet<>(/* initialCapacity= */ 1); eventDispatcher = new MediaSourceEventListener.EventDispatcher(); } /** - * Starts source preparation. This method is called at most once until the next call to {@link + * Starts source preparation and enables the source, see {@link #prepareSource(MediaSourceCaller, + * TransferListener)}. This method is called at most once until the next call to {@link * #releaseSourceInternal()}. * * @param mediaTransferListener The transfer listener which should be informed of any media data @@ -55,9 +58,15 @@ public abstract class BaseMediaSource implements MediaSource { */ protected abstract void prepareSourceInternal(@Nullable TransferListener mediaTransferListener); + /** Enables the source, see {@link #enable(MediaSourceCaller)}. */ + protected void enableInternal() {} + + /** Disables the source, see {@link #disable(MediaSourceCaller)}. */ + protected void disableInternal() {} + /** - * Releases the source. This method is called exactly once after each call to {@link - * #prepareSourceInternal(TransferListener)}. + * Releases the source, see {@link #releaseSource(MediaSourceCaller)}. This method is called + * exactly once after each call to {@link #prepareSourceInternal(TransferListener)}. */ protected abstract void releaseSourceInternal(); @@ -65,13 +74,11 @@ public abstract class BaseMediaSource implements MediaSource { * Updates timeline and manifest and notifies all listeners of the update. * * @param timeline The new {@link Timeline}. - * @param manifest The new manifest. May be null. */ - protected final void refreshSourceInfo(Timeline timeline, @Nullable Object manifest) { + protected final void refreshSourceInfo(Timeline timeline) { this.timeline = timeline; - this.manifest = manifest; - for (SourceInfoRefreshListener listener : sourceInfoListeners) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + for (MediaSourceCaller caller : mediaSourceCallers) { + caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } @@ -118,6 +125,11 @@ public abstract class BaseMediaSource implements MediaSource { return eventDispatcher.withParameters(windowIndex, mediaPeriodId, mediaTimeOffsetMs); } + /** Returns whether the source is enabled. */ + protected final boolean isEnabled() { + return !enabledMediaSourceCallers.isEmpty(); + } + @Override public final void addEventListener(Handler handler, MediaSourceEventListener eventListener) { eventDispatcher.addEventListener(handler, eventListener); @@ -130,27 +142,50 @@ public abstract class BaseMediaSource implements MediaSource { @Override public final void prepareSource( - SourceInfoRefreshListener listener, - @Nullable TransferListener mediaTransferListener) { + MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener) { Looper looper = Looper.myLooper(); Assertions.checkArgument(this.looper == null || this.looper == looper); - sourceInfoListeners.add(listener); + Timeline timeline = this.timeline; + mediaSourceCallers.add(caller); if (this.looper == null) { this.looper = looper; + enabledMediaSourceCallers.add(caller); prepareSourceInternal(mediaTransferListener); } else if (timeline != null) { - listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + enable(caller); + caller.onSourceInfoRefreshed(/* source= */ this, timeline); } } @Override - public final void releaseSource(SourceInfoRefreshListener listener) { - sourceInfoListeners.remove(listener); - if (sourceInfoListeners.isEmpty()) { + public final void enable(MediaSourceCaller caller) { + Assertions.checkNotNull(looper); + boolean wasDisabled = enabledMediaSourceCallers.isEmpty(); + enabledMediaSourceCallers.add(caller); + if (wasDisabled) { + enableInternal(); + } + } + + @Override + public final void disable(MediaSourceCaller caller) { + boolean wasEnabled = !enabledMediaSourceCallers.isEmpty(); + enabledMediaSourceCallers.remove(caller); + if (wasEnabled && enabledMediaSourceCallers.isEmpty()) { + disableInternal(); + } + } + + @Override + public final void releaseSource(MediaSourceCaller caller) { + mediaSourceCallers.remove(caller); + if (mediaSourceCallers.isEmpty()) { looper = null; timeline = null; - manifest = null; + enabledMediaSourceCallers.clear(); releaseSourceInternal(); + } else { + disable(caller); } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index c07805311..4385a41ff 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their @@ -37,8 +39,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb */ public final MediaPeriod mediaPeriod; - private MediaPeriod.Callback callback; - private ClippingSampleStream[] sampleStreams; + @Nullable private MediaPeriod.Callback callback; + private @NullableType ClippingSampleStream[] sampleStreams; private long pendingInitialDiscontinuityPositionUs; /* package */ long startUs; /* package */ long endUs; @@ -95,10 +97,14 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { sampleStreams = new ClippingSampleStream[streams.length]; - SampleStream[] childStreams = new SampleStream[streams.length]; + @NullableType SampleStream[] childStreams = new SampleStream[streams.length]; for (int i = 0; i < streams.length; i++) { sampleStreams[i] = (ClippingSampleStream) streams[i]; childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null; @@ -119,7 +125,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb for (int i = 0; i < streams.length; i++) { if (childStreams[i] == null) { sampleStreams[i] = null; - } else if (streams[i] == null || sampleStreams[i].childStream != childStreams[i]) { + } else if (sampleStreams[i] == null || sampleStreams[i].childStream != childStreams[i]) { sampleStreams[i] = new ClippingSampleStream(childStreams[i]); } streams[i] = sampleStreams[i]; @@ -205,16 +211,21 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return mediaPeriod.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return mediaPeriod.isLoading(); + } + // MediaPeriod.Callback implementation. @Override public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); + Assertions.checkNotNull(callback).onPrepared(this); } @Override public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } /* package */ boolean isPendingInitialDiscontinuity() { @@ -238,7 +249,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } } - private static boolean shouldKeepInitialDiscontinuity(long startUs, TrackSelection[] selections) { + private static boolean shouldKeepInitialDiscontinuity( + long startUs, @NullableType TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer // timestamps can be negative, because sample streams provide buffers starting at a key-frame, @@ -300,7 +312,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } int result = childStream.readData(formatHolder, buffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { - Format format = formatHolder.format; + Format format = Assertions.checkNotNull(formatHolder.format); if (format.encoderDelay != 0 || format.encoderPadding != 0) { // Clear gapless playback metadata if the start/end points don't match the media. int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; @@ -312,7 +324,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ - && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE + && !buffer.waitingForKeys))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; @@ -328,7 +341,5 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } return childStream.skipData(positionUs); } - } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index ce6254e97..4780c075d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -86,9 +86,8 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final ArrayList mediaPeriods; private final Timeline.Window window; - private @Nullable Object manifest; - private ClippingTimeline clippingTimeline; - private IllegalClippingException clippingError; + @Nullable private ClippingTimeline clippingTimeline; + @Nullable private IllegalClippingException clippingError; private long periodStartUs; private long periodEndUs; @@ -192,7 +191,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); prepareChildSource(/* id= */ null, mediaSource); } @@ -222,24 +221,22 @@ public final class ClippingMediaSource extends CompositeMediaSource { Assertions.checkState(mediaPeriods.remove(mediaPeriod)); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); if (mediaPeriods.isEmpty() && !allowDynamicClippingUpdates) { - refreshClippedTimeline(clippingTimeline.timeline); + refreshClippedTimeline(Assertions.checkNotNull(clippingTimeline).timeline); } } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); clippingError = null; clippingTimeline = null; } @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { if (clippingError != null) { return; } - this.manifest = manifest; refreshClippedTimeline(timeline); } @@ -279,7 +276,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { clippingError = e; return; } - refreshSourceInfo(clippingTimeline, manifest); + refreshSourceInfo(clippingTimeline); } @Override @@ -344,10 +341,8 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { - timeline.getWindow( - /* windowIndex= */ 0, window, setTag, /* defaultPositionProjectionUs= */ 0); + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + timeline.getWindow(/* windowIndex= */ 0, window, /* defaultPositionProjectionUs= */ 0); window.positionInFirstPeriodUs += startUs; window.durationUs = durationUs; window.isDynamic = isDynamic; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java index 9323f7505..7077416a0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java @@ -34,17 +34,17 @@ public abstract class CompositeMediaSource extends BaseMediaSource { private final HashMap childSources; - private @Nullable Handler eventHandler; - private @Nullable TransferListener mediaTransferListener; + @Nullable private Handler eventHandler; + @Nullable private TransferListener mediaTransferListener; - /** Create composite media source without child sources. */ + /** Creates composite media source without child sources. */ protected CompositeMediaSource() { childSources = new HashMap<>(); } @Override @CallSuper - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; eventHandler = new Handler(); } @@ -59,9 +59,25 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override @CallSuper - public void releaseSourceInternal() { + protected void enableInternal() { for (MediaSourceAndListener childSource : childSources.values()) { - childSource.mediaSource.releaseSource(childSource.listener); + childSource.mediaSource.enable(childSource.caller); + } + } + + @Override + @CallSuper + protected void disableInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.disable(childSource.caller); + } + } + + @Override + @CallSuper + protected void releaseSourceInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.releaseSource(childSource.caller); childSource.mediaSource.removeEventListener(childSource.eventListener); } childSources.clear(); @@ -73,17 +89,15 @@ public abstract class CompositeMediaSource extends BaseMediaSource { * @param id The unique id used to prepare the child source. * @param mediaSource The child source whose source info has been refreshed. * @param timeline The timeline of the child source. - * @param manifest The manifest of the child source. */ protected abstract void onChildSourceInfoRefreshed( - T id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest); + T id, MediaSource mediaSource, Timeline timeline); /** * Prepares a child source. * - *

{@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline, Object)} will be called - * when the child source updates its timeline and/or manifest with the same {@code id} passed to - * this method. + *

{@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline)} will be called when the + * child source updates its timeline with the same {@code id} passed to this method. * *

Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)} * will be released in {@link #releaseSourceInternal()}. @@ -93,12 +107,35 @@ public abstract class CompositeMediaSource extends BaseMediaSource { */ protected final void prepareChildSource(final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); - SourceInfoRefreshListener sourceListener = - (source, timeline, manifest) -> onChildSourceInfoRefreshed(id, source, timeline, manifest); + MediaSourceCaller caller = + (source, timeline) -> onChildSourceInfoRefreshed(id, source, timeline); MediaSourceEventListener eventListener = new ForwardingEventListener(id); - childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener, eventListener)); + childSources.put(id, new MediaSourceAndListener(mediaSource, caller, eventListener)); mediaSource.addEventListener(Assertions.checkNotNull(eventHandler), eventListener); - mediaSource.prepareSource(sourceListener, mediaTransferListener); + mediaSource.prepareSource(caller, mediaTransferListener); + if (!isEnabled()) { + mediaSource.disable(caller); + } + } + + /** + * Enables a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void enableChildSource(final T id) { + MediaSourceAndListener enabledChild = Assertions.checkNotNull(childSources.get(id)); + enabledChild.mediaSource.enable(enabledChild.caller); + } + + /** + * Disables a child source. + * + * @param id The unique id used to prepare the child source. + */ + protected final void disableChildSource(final T id) { + MediaSourceAndListener disabledChild = Assertions.checkNotNull(childSources.get(id)); + disabledChild.mediaSource.disable(disabledChild.caller); } /** @@ -108,7 +145,7 @@ public abstract class CompositeMediaSource extends BaseMediaSource { */ protected final void releaseChildSource(T id) { MediaSourceAndListener removedChild = Assertions.checkNotNull(childSources.remove(id)); - removedChild.mediaSource.releaseSource(removedChild.listener); + removedChild.mediaSource.releaseSource(removedChild.caller); removedChild.mediaSource.removeEventListener(removedChild.eventListener); } @@ -151,18 +188,28 @@ public abstract class CompositeMediaSource extends BaseMediaSource { return mediaTimeMs; } + /** + * Returns whether {@link MediaSourceEventListener#onMediaPeriodCreated(int, MediaPeriodId)} and + * {@link MediaSourceEventListener#onMediaPeriodReleased(int, MediaPeriodId)} events of the given + * media period should be reported. The default implementation is to always report these events. + * + * @param mediaPeriodId A {@link MediaPeriodId} in the composite media source. + * @return Whether create and release events for this media period should be reported. + */ + protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) { + return true; + } + private static final class MediaSourceAndListener { public final MediaSource mediaSource; - public final SourceInfoRefreshListener listener; + public final MediaSourceCaller caller; public final MediaSourceEventListener eventListener; public MediaSourceAndListener( - MediaSource mediaSource, - SourceInfoRefreshListener listener, - MediaSourceEventListener eventListener) { + MediaSource mediaSource, MediaSourceCaller caller, MediaSourceEventListener eventListener) { this.mediaSource = mediaSource; - this.listener = listener; + this.caller = caller; this.eventListener = eventListener; } } @@ -180,14 +227,20 @@ public abstract class CompositeMediaSource extends BaseMediaSource { @Override public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodCreated(); + if (shouldDispatchCreateOrReleaseEvent( + Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) { + eventDispatcher.mediaPeriodCreated(); + } } } @Override public void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { - eventDispatcher.mediaPeriodReleased(); + if (shouldDispatchCreateOrReleaseEvent( + Assertions.checkNotNull(eventDispatcher.mediaPeriodId))) { + eventDispatcher.mediaPeriodReleased(); + } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index c41933b48..b58370517 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -83,4 +83,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader { return madeProgress; } + @Override + public boolean isLoading() { + for (SequenceableLoader loader : loaders) { + if (loader.isLoading()) { + return true; + } + } + return false; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index e73fdd58a..8dfea1e51 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -18,9 +18,7 @@ package com.google.android.exoplayer2.source; import android.os.Handler; import android.os.Message; import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; @@ -37,6 +35,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -46,7 +45,7 @@ import java.util.Set; * during playback. It is valid for the same {@link MediaSource} instance to be present more than * once in the concatenation. Access to this class is thread-safe. */ -public class ConcatenatingMediaSource extends CompositeMediaSource { +public final class ConcatenatingMediaSource extends CompositeMediaSource { private static final int MSG_ADD = 0; private static final int MSG_REMOVE = 1; @@ -70,16 +69,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders; private final Map mediaSourceByMediaPeriod; private final Map mediaSourceByUid; + private final Set enabledMediaSourceHolders; private final boolean isAtomic; private final boolean useLazyPreparation; - private final Timeline.Window window; - private final Timeline.Period period; private boolean timelineUpdateScheduled; private Set nextTimelineUpdateOnCompletionActions; private ShuffleOrder shuffleOrder; - private int windowCount; - private int periodCount; /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same @@ -137,10 +133,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); this.nextTimelineUpdateOnCompletionActions = new HashSet<>(); this.pendingOnCompletionActions = new HashSet<>(); + this.enabledMediaSourceHolders = new HashSet<>(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; - window = new Timeline.Window(); - period = new Timeline.Period(); addMediaSources(Arrays.asList(mediaSources)); } @@ -149,7 +144,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { + public synchronized void addMediaSources(Collection mediaSources) { addPublicMediaSources( mediaSourcesPublic.size(), mediaSources, @@ -221,7 +216,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, Runnable onCompletionAction) { addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); } @@ -234,7 +229,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { + public synchronized void addMediaSources(int index, Collection mediaSources) { addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); } @@ -249,7 +244,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, @@ -268,9 +263,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders = new ArrayList<>(mediaSources.size()); for (MediaSource mediaSource : mediaSources) { - mediaSourceHolders.add(new MediaSourceHolder(mediaSource)); + mediaSourceHolders.add(new MediaSourceHolder(mediaSource, useLazyPreparation)); } mediaSourcesPublic.addAll(index, mediaSourceHolders); if (playbackThreadHandler != null && !mediaSources.isEmpty()) { @@ -702,10 +704,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource onCompletionActions = nextTimelineUpdateOnCompletionActions; nextTimelineUpdateOnCompletionActions = new HashSet<>(); - refreshSourceInfo( - new ConcatenatedTimeline( - mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), - /* manifest= */ null); + refreshSourceInfo(new ConcatenatedTimeline(mediaSourceHolders, shuffleOrder, isAtomic)); getPlaybackThreadHandlerOnPlaybackThread() .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) .sendToTarget(); @@ -736,24 +735,21 @@ public class ConcatenatingMediaSource extends CompositeMediaSource 0) { MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + Timeline previousTimeline = previousHolder.mediaSource.getTimeline(); newMediaSourceHolder.reset( - newIndex, - previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), - previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount()); + newIndex, previousHolder.firstWindowIndexInChild + previousTimeline.getWindowCount()); } else { - newMediaSourceHolder.reset( - newIndex, /* firstWindowIndexInChild= */ 0, /* firstPeriodIndexInChild= */ 0); + newMediaSourceHolder.reset(newIndex, /* firstWindowIndexInChild= */ 0); } - correctOffsets( - newIndex, - /* childIndexUpdate= */ 1, - newMediaSourceHolder.timeline.getWindowCount(), - newMediaSourceHolder.timeline.getPeriodCount()); + Timeline newTimeline = newMediaSourceHolder.mediaSource.getTimeline(); + correctOffsets(newIndex, /* childIndexUpdate= */ 1, newTimeline.getWindowCount()); mediaSourceHolders.add(newIndex, newMediaSourceHolder); mediaSourceByUid.put(newMediaSourceHolder.uid, newMediaSourceHolder); - if (!useLazyPreparation) { - newMediaSourceHolder.hasStartedPreparing = true; - prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); + prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); + if (isEnabled() && mediaSourceByMediaPeriod.isEmpty()) { + enabledMediaSourceHolders.add(newMediaSourceHolder); + } else { + disableChildSource(newMediaSourceHolder); } } @@ -761,79 +757,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource periodPosition = - timeline.getPeriodPosition(window, period, /* windowIndex= */ 0, windowStartPositionUs); - Object periodUid = periodPosition.first; - long periodPositionUs = periodPosition.second; - mediaSourceHolder.timeline = DeferredTimeline.createWithRealTimeline(timeline, periodUid); - if (deferredMediaPeriod != null) { - deferredMediaPeriod.overridePreparePositionUs(periodPositionUs); - MediaPeriodId idInSource = - deferredMediaPeriod.id.copyWithPeriodUid( - getChildPeriodUid(mediaSourceHolder, deferredMediaPeriod.id.periodUid)); - deferredMediaPeriod.createPeriod(idInSource); + if (mediaSourceHolder.childIndex + 1 < mediaSourceHolders.size()) { + MediaSourceHolder nextHolder = mediaSourceHolders.get(mediaSourceHolder.childIndex + 1); + int windowOffsetUpdate = + timeline.getWindowCount() + - (nextHolder.firstWindowIndexInChild - mediaSourceHolder.firstWindowIndexInChild); + if (windowOffsetUpdate != 0) { + correctOffsets( + mediaSourceHolder.childIndex + 1, /* childIndexUpdate= */ 0, windowOffsetUpdate); } } - mediaSourceHolder.isPrepared = true; scheduleTimelineUpdate(); } private void removeMediaSourceInternal(int index) { MediaSourceHolder holder = mediaSourceHolders.remove(index); mediaSourceByUid.remove(holder.uid); - Timeline oldTimeline = holder.timeline; - correctOffsets( - index, - /* childIndexUpdate= */ -1, - -oldTimeline.getWindowCount(), - -oldTimeline.getPeriodCount()); + Timeline oldTimeline = holder.mediaSource.getTimeline(); + correctOffsets(index, /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount()); holder.isRemoved = true; maybeReleaseChildSource(holder); } @@ -842,91 +783,85 @@ public class ConcatenatingMediaSource extends CompositeMediaSource iterator = enabledMediaSourceHolders.iterator(); + while (iterator.hasNext()) { + MediaSourceHolder holder = iterator.next(); + if (holder.activeMediaPeriodIds.isEmpty()) { + disableChildSource(holder); + iterator.remove(); + } + } + } + /** Return uid of media source holder from period uid of concatenated source. */ private static Object getMediaSourceHolderUid(Object periodUid) { return ConcatenatedTimeline.getChildTimelineUidFromConcatenatedUid(periodUid); } /** Return uid of child period from period uid of concatenated source. */ - private static Object getChildPeriodUid(MediaSourceHolder holder, Object periodUid) { - Object childUid = ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); - return childUid.equals(DeferredTimeline.DUMMY_ID) ? holder.timeline.replacedId : childUid; + private static Object getChildPeriodUid(Object periodUid) { + return ConcatenatedTimeline.getChildPeriodUidFromConcatenatedUid(periodUid); } private static Object getPeriodUid(MediaSourceHolder holder, Object childPeriodUid) { - if (holder.timeline.replacedId.equals(childPeriodUid)) { - childPeriodUid = DeferredTimeline.DUMMY_ID; - } return ConcatenatedTimeline.getConcatenatedUid(holder.uid, childPeriodUid); } /** Data class to hold playlist media sources together with meta data needed to process them. */ - /* package */ static final class MediaSourceHolder implements Comparable { + /* package */ static final class MediaSourceHolder { - public final MediaSource mediaSource; + public final MaskingMediaSource mediaSource; public final Object uid; - public final List activeMediaPeriods; + public final List activeMediaPeriodIds; - public DeferredTimeline timeline; public int childIndex; public int firstWindowIndexInChild; - public int firstPeriodIndexInChild; - public boolean hasStartedPreparing; - public boolean isPrepared; public boolean isRemoved; - public MediaSourceHolder(MediaSource mediaSource) { - this.mediaSource = mediaSource; - this.timeline = DeferredTimeline.createWithDummyTimeline(mediaSource.getTag()); - this.activeMediaPeriods = new ArrayList<>(); + public MediaSourceHolder(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = new MaskingMediaSource(mediaSource, useLazyPreparation); + this.activeMediaPeriodIds = new ArrayList<>(); this.uid = new Object(); } - public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) { + public void reset(int childIndex, int firstWindowIndexInChild) { this.childIndex = childIndex; this.firstWindowIndexInChild = firstWindowIndexInChild; - this.firstPeriodIndexInChild = firstPeriodIndexInChild; - this.hasStartedPreparing = false; - this.isPrepared = false; this.isRemoved = false; - this.activeMediaPeriods.clear(); - } - - @Override - public int compareTo(@NonNull MediaSourceHolder other) { - return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; + this.activeMediaPeriodIds.clear(); } } @@ -957,13 +892,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceHolders, - int windowCount, - int periodCount, ShuffleOrder shuffleOrder, boolean isAtomic) { super(isAtomic, shuffleOrder); - this.windowCount = windowCount; - this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); firstPeriodInChildIndices = new int[childCount]; firstWindowInChildIndices = new int[childCount]; @@ -971,13 +902,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); int index = 0; + int windowCount = 0; + int periodCount = 0; for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - timelines[index] = mediaSourceHolder.timeline; - firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; - firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; + timelines[index] = mediaSourceHolder.mediaSource.getTimeline(); + firstWindowInChildIndices[index] = windowCount; + firstPeriodInChildIndices[index] = periodCount; + windowCount += timelines[index].getWindowCount(); + periodCount += timelines[index].getPeriodCount(); uids[index] = mediaSourceHolder.uid; childIndexByUid.put(uids[index], index++); } + this.windowCount = windowCount; + this.periodCount = periodCount; } @Override @@ -1027,135 +964,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { /** @deprecated Use {@link MediaSourceEventListener} instead. */ @Deprecated @@ -59,15 +58,15 @@ public final class ExtractorMediaSource extends BaseMediaSource } - /** Use {@link ProgressiveMediaSource.Factory} instead. */ + /** @deprecated Use {@link ProgressiveMediaSource.Factory} instead. */ @Deprecated - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; - private @Nullable ExtractorsFactory extractorsFactory; - private @Nullable String customCacheKey; - private @Nullable Object tag; + @Nullable private ExtractorsFactory extractorsFactory; + @Nullable private String customCacheKey; + @Nullable private Object tag; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; @@ -180,6 +179,13 @@ public final class ExtractorMediaSource extends BaseMediaSource return this; } + /** @deprecated Use {@link ProgressiveMediaSource.Factory#setDrmSessionManager} instead. */ + @Override + @Deprecated + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + throw new UnsupportedOperationException(); + } + /** * Returns a new {@link ExtractorMediaSource} using the current parameters. * @@ -222,6 +228,9 @@ public final class ExtractorMediaSource extends BaseMediaSource } } + /** + * @deprecated Use {@link ProgressiveMediaSource#DEFAULT_LOADING_CHECK_INTERVAL_BYTES} instead. + */ @Deprecated public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES; @@ -243,8 +252,8 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } @@ -265,9 +274,9 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener, - String customCacheKey) { + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + @Nullable String customCacheKey) { this( uri, dataSourceFactory, @@ -297,9 +306,9 @@ public final class ExtractorMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, - Handler eventHandler, - EventListener eventListener, - String customCacheKey, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener, + @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes) { this( uri, @@ -327,6 +336,7 @@ public final class ExtractorMediaSource extends BaseMediaSource uri, dataSourceFactory, extractorsFactory, + DrmSessionManager.getDummyDrmSessionManager(), loadableLoadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, @@ -340,13 +350,15 @@ public final class ExtractorMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener); + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + prepareChildSource(/* id= */ null, progressiveMediaSource); } @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - progressiveMediaSource.maybeThrowSourceInfoRefreshError(); + protected void onChildSourceInfoRefreshed( + @Nullable Void id, MediaSource mediaSource, Timeline timeline) { + refreshSourceInfo(timeline); } @Override @@ -359,19 +371,8 @@ public final class ExtractorMediaSource extends BaseMediaSource progressiveMediaSource.releasePeriod(mediaPeriod); } - @Override - public void releaseSourceInternal() { - progressiveMediaSource.releaseSource(/* listener= */ this); - } - - @Override - public void onSourceInfoRefreshed( - MediaSource source, Timeline timeline, @Nullable Object manifest) { - refreshSourceInfo(timeline, manifest); - } - @Deprecated - private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { + private static final class EventListenerWrapper implements MediaSourceEventListener { private final EventListener eventListener; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java index 45997aced..38b373b26 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -57,9 +57,8 @@ public abstract class ForwardingTimeline extends Timeline { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { - return timeline.getWindow(windowIndex, window, setTag, defaultPositionProjectionUs); + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + return timeline.getWindow(windowIndex, window, defaultPositionProjectionUs); } @Override diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java index 3d31e2a77..d09707396 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/IcyDataSource.java @@ -110,7 +110,7 @@ import java.util.Map; /** * Reads an ICY stream metadata block, passing it to {@link #listener} unless the block is empty. * - * @return True if the block was extracted, including if it's length byte indicated a length of + * @return True if the block was extracted, including if its length byte indicated a length of * zero. False if the end of the stream was reached. * @throws IOException If an error occurs reading from the wrapped {@link DataSource}. */ diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 769f545aa..ac23e2a83 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -71,7 +71,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); prepareChildSource(/* id= */ null, childSource); } @@ -100,13 +100,12 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + protected void onChildSourceInfoRefreshed(Void id, MediaSource mediaSource, Timeline timeline) { Timeline loopingTimeline = loopCount != Integer.MAX_VALUE ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline); - refreshSourceInfo(loopingTimeline, manifest); + refreshSourceInfo(loopingTimeline); } @Override diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java similarity index 78% rename from TMessagesProj/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java rename to TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java index abf02541c..17ac6c066 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MaskingMediaPeriod.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; @@ -22,6 +24,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Media period that wraps a media source and defers calling its {@link @@ -29,7 +32,7 @@ import java.io.IOException; * #createPeriod(MediaPeriodId)} has been called. This is useful if you need to return a media * period immediately but the media source that should create it is not yet prepared. */ -public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { +public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { /** Listener for preparation errors. */ public interface PrepareErrorListener { @@ -42,27 +45,27 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** The {@link MediaSource} which will create the actual media period. */ public final MediaSource mediaSource; - /** The {@link MediaPeriodId} used to create the deferred media period. */ + /** The {@link MediaPeriodId} used to create the masking media period. */ public final MediaPeriodId id; private final Allocator allocator; - private MediaPeriod mediaPeriod; - private Callback callback; + @Nullable private MediaPeriod mediaPeriod; + @Nullable private Callback callback; private long preparePositionUs; - private @Nullable PrepareErrorListener listener; + @Nullable private PrepareErrorListener listener; private boolean notifiedPrepareError; private long preparePositionOverrideUs; /** - * Creates a new deferred media period. + * Creates a new masking media period. * * @param mediaSource The media source to wrap. - * @param id The identifier used to create the deferred media period. + * @param id The identifier used to create the masking media period. * @param allocator The allocator used to create the media period. * @param preparePositionUs The expected start position, in microseconds. */ - public DeferredMediaPeriod( + public MaskingMediaPeriod( MediaSource mediaSource, MediaPeriodId id, Allocator allocator, long preparePositionUs) { this.id = id; this.allocator = allocator; @@ -82,7 +85,7 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb this.listener = listener; } - /** Returns the position at which the deferred media period was prepared, in microseconds. */ + /** Returns the position at which the masking media period was prepared, in microseconds. */ public long getPreparePositionUs() { return preparePositionUs; } @@ -150,53 +153,57 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); + return castNonNull(mediaPeriod).getTrackGroups(); } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == preparePositionUs) { positionUs = preparePositionOverrideUs; preparePositionOverrideUs = C.TIME_UNSET; } - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); + return castNonNull(mediaPeriod) + .selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs); } @Override public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); + castNonNull(mediaPeriod).discardBuffer(positionUs, toKeyframe); } @Override public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); + return castNonNull(mediaPeriod).readDiscontinuity(); } @Override public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); + return castNonNull(mediaPeriod).getBufferedPositionUs(); } @Override public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); + return castNonNull(mediaPeriod).seekToUs(positionUs); } @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); + return castNonNull(mediaPeriod).getAdjustedSeekPositionUs(positionUs, seekParameters); } @Override public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); + return castNonNull(mediaPeriod).getNextLoadPositionUs(); } @Override public void reevaluateBuffer(long positionUs) { - mediaPeriod.reevaluateBuffer(positionUs); + castNonNull(mediaPeriod).reevaluateBuffer(positionUs); } @Override @@ -204,16 +211,21 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return mediaPeriod != null && mediaPeriod.isLoading(); + } + @Override public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); + castNonNull(callback).onContinueLoadingRequested(this); } // MediaPeriod.Callback implementation @Override public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); + castNonNull(callback).onPrepared(this); } private long getPreparePositionWithOverride(long preparePositionUs) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java new file mode 100644 index 000000000..47279f235 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import android.util.Pair; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; + +/** + * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media + * structure is known. + */ +public final class MaskingMediaSource extends CompositeMediaSource { + + private final MediaSource mediaSource; + private final boolean useLazyPreparation; + private final Timeline.Window window; + private final Timeline.Period period; + + private MaskingTimeline timeline; + @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod; + @Nullable private EventDispatcher unpreparedMaskingMediaPeriodEventDispatcher; + private boolean hasStartedPreparing; + private boolean isPrepared; + + /** + * Creates the masking media source. + * + * @param mediaSource A {@link MediaSource}. + * @param useLazyPreparation Whether the {@code mediaSource} is prepared lazily. If false, all + * manifest loads and other initial preparation steps happen immediately. If true, these + * initial preparations are triggered only when the player starts buffering the media. + */ + public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) { + this.mediaSource = mediaSource; + this.useLazyPreparation = useLazyPreparation; + window = new Timeline.Window(); + period = new Timeline.Period(); + timeline = MaskingTimeline.createWithDummyTimeline(mediaSource.getTag()); + } + + /** Returns the {@link Timeline}. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + super.prepareSourceInternal(mediaTransferListener); + if (!useLazyPreparation) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + + @Nullable + @Override + public Object getTag() { + return mediaSource.getTag(); + } + + @Override + @SuppressWarnings("MissingSuperCall") + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. Source info refresh errors will be thrown when calling + // MaskingMediaPeriod.maybeThrowPrepareError. + } + + @Override + public MaskingMediaPeriod createPeriod( + MediaPeriodId id, Allocator allocator, long startPositionUs) { + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(mediaSource, id, allocator, startPositionUs); + if (isPrepared) { + MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid)); + mediaPeriod.createPeriod(idInSource); + } else { + // We should have at most one media period while source is unprepared because the duration is + // unset and we don't load beyond periods with unset duration. We need to figure out how to + // handle the prepare positions of multiple deferred media periods, should that ever change. + unpreparedMaskingMediaPeriod = mediaPeriod; + unpreparedMaskingMediaPeriodEventDispatcher = + createEventDispatcher(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0); + unpreparedMaskingMediaPeriodEventDispatcher.mediaPeriodCreated(); + if (!hasStartedPreparing) { + hasStartedPreparing = true; + prepareChildSource(/* id= */ null, mediaSource); + } + } + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((MaskingMediaPeriod) mediaPeriod).releasePeriod(); + if (mediaPeriod == unpreparedMaskingMediaPeriod) { + Assertions.checkNotNull(unpreparedMaskingMediaPeriodEventDispatcher).mediaPeriodReleased(); + unpreparedMaskingMediaPeriodEventDispatcher = null; + unpreparedMaskingMediaPeriod = null; + } + } + + @Override + public void releaseSourceInternal() { + isPrepared = false; + hasStartedPreparing = false; + super.releaseSourceInternal(); + } + + @Override + protected void onChildSourceInfoRefreshed( + Void id, MediaSource mediaSource, Timeline newTimeline) { + if (isPrepared) { + timeline = timeline.cloneWithUpdatedTimeline(newTimeline); + } else if (newTimeline.isEmpty()) { + timeline = + MaskingTimeline.createWithRealTimeline( + newTimeline, Window.SINGLE_WINDOW_UID, MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID); + } else { + // Determine first period and the start position. + // This will be: + // 1. The default window start position if no deferred period has been created yet. + // 2. The non-zero prepare position of the deferred period under the assumption that this is + // a non-zero initial seek position in the window. + // 3. The default window start position if the deferred period has a prepare position of zero + // under the assumption that the prepare position of zero was used because it's the + // default position of the DummyTimeline window. Note that this will override an + // intentional seek to zero for a window with a non-zero default position. This is + // unlikely to be a problem as a non-zero default position usually only occurs for live + // playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions + // anyway. + newTimeline.getWindow(/* windowIndex= */ 0, window); + long windowStartPositionUs = window.getDefaultPositionUs(); + if (unpreparedMaskingMediaPeriod != null) { + long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs(); + if (periodPreparePositionUs != 0) { + windowStartPositionUs = periodPreparePositionUs; + } + } + Object windowUid = window.uid; + Pair periodPosition = + newTimeline.getPeriodPosition( + window, period, /* windowIndex= */ 0, windowStartPositionUs); + Object periodUid = periodPosition.first; + long periodPositionUs = periodPosition.second; + timeline = MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid); + if (unpreparedMaskingMediaPeriod != null) { + MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod; + maskingPeriod.overridePreparePositionUs(periodPositionUs); + MediaPeriodId idInSource = + maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid)); + maskingPeriod.createPeriod(idInSource); + } + } + isPrepared = true; + refreshSourceInfo(this.timeline); + } + + @Nullable + @Override + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + Void id, MediaPeriodId mediaPeriodId) { + return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid)); + } + + @Override + protected boolean shouldDispatchCreateOrReleaseEvent(MediaPeriodId mediaPeriodId) { + // Suppress create and release events for the period created while the source was still + // unprepared, as we send these events from this class. + return unpreparedMaskingMediaPeriod == null + || !mediaPeriodId.equals(unpreparedMaskingMediaPeriod.id); + } + + private Object getInternalPeriodUid(Object externalPeriodUid) { + return externalPeriodUid.equals(MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID) + ? timeline.replacedInternalPeriodUid + : externalPeriodUid; + } + + private Object getExternalPeriodUid(Object internalPeriodUid) { + return timeline.replacedInternalPeriodUid.equals(internalPeriodUid) + ? MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID + : internalPeriodUid; + } + + /** + * Timeline used as placeholder for an unprepared media source. After preparation, a + * MaskingTimeline is used to keep the originally assigned dummy period ID. + */ + private static final class MaskingTimeline extends ForwardingTimeline { + + public static final Object DUMMY_EXTERNAL_PERIOD_UID = new Object(); + + private final Object replacedInternalWindowUid; + private final Object replacedInternalPeriodUid; + + /** + * Returns an instance with a dummy timeline using the provided window tag. + * + * @param windowTag A window tag. + */ + public static MaskingTimeline createWithDummyTimeline(@Nullable Object windowTag) { + return new MaskingTimeline( + new DummyTimeline(windowTag), Window.SINGLE_WINDOW_UID, DUMMY_EXTERNAL_PERIOD_UID); + } + + /** + * Returns an instance with a real timeline, replacing the provided period ID with the already + * assigned dummy period ID. + * + * @param timeline The real timeline. + * @param firstWindowUid The window UID in the timeline which will be replaced by the already + * assigned {@link Window#SINGLE_WINDOW_UID}. + * @param firstPeriodUid The period UID in the timeline which will be replaced by the already + * assigned {@link #DUMMY_EXTERNAL_PERIOD_UID}. + */ + public static MaskingTimeline createWithRealTimeline( + Timeline timeline, Object firstWindowUid, Object firstPeriodUid) { + return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid); + } + + private MaskingTimeline( + Timeline timeline, Object replacedInternalWindowUid, Object replacedInternalPeriodUid) { + super(timeline); + this.replacedInternalWindowUid = replacedInternalWindowUid; + this.replacedInternalPeriodUid = replacedInternalPeriodUid; + } + + /** + * Returns a copy with an updated timeline. This keeps the existing period replacement. + * + * @param timeline The new timeline. + */ + public MaskingTimeline cloneWithUpdatedTimeline(Timeline timeline) { + return new MaskingTimeline(timeline, replacedInternalWindowUid, replacedInternalPeriodUid); + } + + /** Returns the wrapped timeline. */ + public Timeline getTimeline() { + return timeline; + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + timeline.getWindow(windowIndex, window, defaultPositionProjectionUs); + if (Util.areEqual(window.uid, replacedInternalWindowUid)) { + window.uid = Window.SINGLE_WINDOW_UID; + } + return window; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + timeline.getPeriod(periodIndex, period, setIds); + if (Util.areEqual(period.uid, replacedInternalPeriodUid)) { + period.uid = DUMMY_EXTERNAL_PERIOD_UID; + } + return period; + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod( + DUMMY_EXTERNAL_PERIOD_UID.equals(uid) ? replacedInternalPeriodUid : uid); + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + Object uid = timeline.getUidOfPeriod(periodIndex); + return Util.areEqual(uid, replacedInternalPeriodUid) ? DUMMY_EXTERNAL_PERIOD_UID : uid; + } + } + + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + public static final class DummyTimeline extends Timeline { + + @Nullable private final Object tag; + + public DummyTimeline(@Nullable Object tag) { + this.tag = tag; + } + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { + return window.set( + Window.SINGLE_WINDOW_UID, + tag, + /* manifest= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + // Dynamic window to indicate pending timeline updates. + /* isDynamic= */ true, + /* isLive= */ false, + /* defaultPositionUs= */ 0, + /* durationUs= */ C.TIME_UNSET, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, + /* positionInFirstPeriodUs= */ 0); + } + + @Override + public int getPeriodCount() { + return 1; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return period.set( + /* id= */ 0, + /* uid= */ MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID ? 0 : C.INDEX_UNSET; + } + + @Override + public Object getUidOfPeriod(int periodIndex) { + return MaskingTimeline.DUMMY_EXTERNAL_PERIOD_UID; + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index c84847f75..2e2cf9cab 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; import java.util.Collections; @@ -57,9 +58,8 @@ public interface MediaPeriod extends SequenceableLoader { * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. * *

If preparation succeeds and results in a source timeline change (e.g. the period duration - * becoming known), {@link - * MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} - * will be called before {@code callback.onPrepared}. + * becoming known), {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be + * called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. @@ -113,15 +113,17 @@ public interface MediaPeriod extends SequenceableLoader { * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. * - *

Note that previously received {@link TrackSelection TrackSelections} are no longer valid and - * references need to be replaced even if the corresponding {@link SampleStream} is kept. + *

Note that previously passed {@link TrackSelection TrackSelections} are no longer valid, and + * any references to them must be updated to point to the new selections. * *

This method is only called after the period has been prepared. * * @param selections The renderer track selections. * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained - * for each selection. A {@code true} value indicates that the selection is unchanged, and - * that the caller does not require that the sample stream be recreated. + * for each track selection. A {@code true} value indicates that the selection is equivalent + * to the one that was previously passed, and that the caller does not require that the sample + * stream be recreated. If a retained sample stream holds any references to the track + * selection then they must be updated to point to the new selection. * @param streams The existing sample streams, which will be updated to reflect the provided * selections. * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that @@ -229,6 +231,9 @@ public interface MediaPeriod extends SequenceableLoader { @Override boolean continueLoading(long positionUs); + /** Returns whether the media period is currently loading. */ + boolean isLoading(); + /** * Re-evaluates the buffer given the playback position. * diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index d7b7c7521..5ee980d01 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source; import android.os.Handler; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; @@ -31,9 +30,9 @@ import java.io.IOException; *

    *
  • To provide the player with a {@link Timeline} defining the structure of its media, and to * provide a new timeline whenever the structure of the media changes. The MediaSource - * provides these timelines by calling {@link SourceInfoRefreshListener#onSourceInfoRefreshed} - * on the {@link SourceInfoRefreshListener}s passed to {@link - * #prepareSource(SourceInfoRefreshListener, TransferListener)}. + * provides these timelines by calling {@link MediaSourceCaller#onSourceInfoRefreshed} on the + * {@link MediaSourceCaller}s passed to {@link #prepareSource(MediaSourceCaller, + * TransferListener)}. *
  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a * way for the player to load and read the media. @@ -46,25 +45,21 @@ import java.io.IOException; */ public interface MediaSource { - /** Listener for source events. */ - interface SourceInfoRefreshListener { + /** A caller of media sources, which will be notified of source events. */ + interface MediaSourceCaller { /** - * Called when manifest and/or timeline has been refreshed. - *

    - * Called on the playback thread. + * Called when the {@link Timeline} has been refreshed. + * + *

    Called on the playback thread. * * @param source The {@link MediaSource} whose info has been refreshed. * @param timeline The source's timeline. - * @param manifest The loaded manifest. May be null. */ - void onSourceInfoRefreshed(MediaSource source, Timeline timeline, @Nullable Object manifest); - + void onSourceInfoRefreshed(MediaSource source, Timeline timeline); } - /** - * Identifier for a {@link MediaPeriod}. - */ + /** Identifier for a {@link MediaPeriod}. */ final class MediaPeriodId { /** The unique id of the timeline period. */ @@ -240,38 +235,52 @@ public interface MediaSource { } /** - * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest - * updates. + * Registers a {@link MediaSourceCaller}. Starts source preparation if needed and enables the + * source for the creation of {@link MediaPeriod MediaPerods}. * *

    Should not be called directly from application code. * - *

    The listener will be also be notified if the source already has a timeline and/or manifest. + *

    {@link MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} will be called once + * the source has a {@link Timeline}. * - *

    For each call to this method, a call to {@link #releaseSource(SourceInfoRefreshListener)} is - * needed to remove the listener and to release the source if no longer required. + *

    For each call to this method, a call to {@link #releaseSource(MediaSourceCaller)} is needed + * to remove the caller and to release the source if no longer required. * - * @param listener The listener to be added. + * @param caller The {@link MediaSourceCaller} to be registered. * @param mediaTransferListener The transfer listener which should be informed of any media data * transfers. May be null if no listener is available. Note that this listener should be only * informed of transfers related to the media loads and not of auxiliary loads for manifests * and other data. */ - void prepareSource( - SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener); + void prepareSource(MediaSourceCaller caller, @Nullable TransferListener mediaTransferListener); /** * Throws any pending error encountered while loading or refreshing source information. - *

    - * Should not be called directly from application code. + * + *

    Should not be called directly from application code. + * + *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. */ void maybeThrowSourceInfoRefreshError() throws IOException; /** - * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called - * multiple times without an intervening call to {@link #releasePeriod(MediaPeriod)}. + * Enables the source for the creation of {@link MediaPeriod MediaPeriods}. * *

    Should not be called directly from application code. * + *

    Must only be called after {@link #prepareSource(MediaSourceCaller, TransferListener)}. + * + * @param caller The {@link MediaSourceCaller} enabling the source. + */ + void enable(MediaSourceCaller caller); + + /** + * Returns a new {@link MediaPeriod} identified by {@code periodId}. + * + *

    Should not be called directly from application code. + * + *

    Must only be called if the source is enabled. + * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param startPositionUs The expected start position, in microseconds. @@ -281,20 +290,36 @@ public interface MediaSource { /** * Releases the period. - *

    - * Should not be called directly from application code. + * + *

    Should not be called directly from application code. * * @param mediaPeriod The period to release. */ void releasePeriod(MediaPeriod mediaPeriod); /** - * Removes a listener for timeline and/or manifest updates and releases the source if no longer - * required. + * Disables the source for the creation of {@link MediaPeriod MediaPeriods}. The implementation + * should not hold onto limited resources used for the creation of media periods. * *

    Should not be called directly from application code. * - * @param listener The listener to be removed. + *

    Must only be called after all {@link MediaPeriod MediaPeriods} previously created by {@link + * #createPeriod(MediaPeriodId, Allocator, long)} have been released by {@link + * #releasePeriod(MediaPeriod)}. + * + * @param caller The {@link MediaSourceCaller} disabling the source. */ - void releaseSource(SourceInfoRefreshListener listener); + void disable(MediaSourceCaller caller); + + /** + * Unregisters a caller, and disables and releases the source if no longer required. + * + *

    Should not be called directly from application code. + * + *

    Must only be called if all created {@link MediaPeriod MediaPeriods} have been released by + * {@link #releasePeriod(MediaPeriod)}. + * + * @param caller The {@link MediaSourceCaller} to be unregistered. + */ + void releaseSource(MediaSourceCaller caller); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 233e19b29..9e6f4f9cf 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -101,7 +101,7 @@ public interface MediaSourceEventListener { * The format of the track to which the data belongs. Null if the data does not belong to a * specific track. */ - public final @Nullable Format trackFormat; + @Nullable public final Format trackFormat; /** * One of the {@link C} {@code SELECTION_REASON_*} constants if the data belongs to a track. * {@link C#SELECTION_REASON_UNKNOWN} otherwise. @@ -111,7 +111,7 @@ public interface MediaSourceEventListener { * Optional data associated with the selection of the track to which the data belongs. Null if * the data does not belong to a track. */ - public final @Nullable Object trackSelectionData; + @Nullable public final Object trackSelectionData; /** * The start time of the media, or {@link C#TIME_UNSET} if the data does not belong to a * specific media period. @@ -164,7 +164,7 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the created media period. */ - void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId); + default void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * Called when a media period is released by the media source. @@ -172,7 +172,7 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the released media period. */ - void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId); + default void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * Called when a load begins. @@ -185,11 +185,11 @@ public interface MediaSourceEventListener { * LoadEventInfo#responseHeaders} will be empty. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadStarted( + default void onLoadStarted( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData); + MediaLoadData mediaLoadData) {} /** * Called when a load ends. @@ -203,11 +203,11 @@ public interface MediaSourceEventListener { * event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadCompleted( + default void onLoadCompleted( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData); + MediaLoadData mediaLoadData) {} /** * Called when a load is canceled. @@ -221,11 +221,11 @@ public interface MediaSourceEventListener { * event. * @param mediaLoadData The {@link MediaLoadData} defining the data being loaded. */ - void onLoadCanceled( + default void onLoadCanceled( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, - MediaLoadData mediaLoadData); + MediaLoadData mediaLoadData) {} /** * Called when a load error occurs. @@ -252,13 +252,13 @@ public interface MediaSourceEventListener { * @param error The load error. * @param wasCanceled Whether the load was canceled as a result of the error. */ - void onLoadError( + default void onLoadError( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, - boolean wasCanceled); + boolean wasCanceled) {} /** * Called when a media period is first being read from. @@ -266,7 +266,7 @@ public interface MediaSourceEventListener { * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the media period being read from. */ - void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId); + default void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * Called when data is removed from the back of a media buffer, typically so that it can be @@ -276,8 +276,8 @@ public interface MediaSourceEventListener { * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to. * @param mediaLoadData The {@link MediaLoadData} defining the media being discarded. */ - void onUpstreamDiscarded( - int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData); + default void onUpstreamDiscarded( + int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {} /** * Called when a downstream format change occurs (i.e. when the format of the media being read @@ -287,8 +287,8 @@ public interface MediaSourceEventListener { * @param mediaPeriodId The {@link MediaPeriodId} the media belongs to. * @param mediaLoadData The {@link MediaLoadData} defining the newly selected downstream data. */ - void onDownstreamFormatChanged( - int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData); + default void onDownstreamFormatChanged( + int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {} /** Dispatches events to {@link MediaSourceEventListener}s. */ final class EventDispatcher { @@ -296,7 +296,7 @@ public interface MediaSourceEventListener { /** The timeline window index reported with the events. */ public final int windowIndex; /** The {@link MediaPeriodId} reported with the events. */ - public final @Nullable MediaPeriodId mediaPeriodId; + @Nullable public final MediaPeriodId mediaPeriodId; private final CopyOnWriteArrayList listenerAndHandlers; private final long mediaTimeOffsetMs; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java new file mode 100644 index 000000000..201f241d5 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.offline.StreamKey; +import java.util.List; + +/** Factory for creating {@link MediaSource}s from URIs. */ +public interface MediaSourceFactory { + + /** + * Sets a list of {@link StreamKey StreamKeys} by which the manifest is filtered. + * + * @param streamKeys A list of {@link StreamKey StreamKeys}. + * @return This factory, for convenience. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. + */ + default MediaSourceFactory setStreamKeys(List streamKeys) { + return this; + } + + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + MediaSourceFactory setDrmSessionManager(DrmSessionManager drmSessionManager); + + /** + * Creates a new {@link MediaSource} with the specified {@code uri}. + * + * @param uri The URI to play. + * @return The new {@link MediaSource media source}. + */ + MediaSource createMediaSource(Uri uri); + + /** + * Returns the {@link C.ContentType content types} supported by media sources created by this + * factory. + */ + @C.ContentType + int[] getSupportedTypes(); +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index a4fc8c6b0..afa25d6fc 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -23,6 +24,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Merges multiple {@link MediaPeriod}s. @@ -35,9 +37,8 @@ import java.util.IdentityHashMap; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final ArrayList childrenPendingPreparation; - private Callback callback; - private TrackGroupArray trackGroups; - + @Nullable private Callback callback; + @Nullable private TrackGroupArray trackGroups; private MediaPeriod[] enabledPeriods; private SequenceableLoader compositeSequenceableLoader; @@ -49,6 +50,7 @@ import java.util.IdentityHashMap; compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(); streamPeriodIndices = new IdentityHashMap<>(); + enabledPeriods = new MediaPeriod[0]; } @Override @@ -69,12 +71,16 @@ import java.util.IdentityHashMap; @Override public TrackGroupArray getTrackGroups() { - return trackGroups; + return Assertions.checkNotNull(trackGroups); } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { // Map each selection and stream onto a child period index. int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; @@ -94,9 +100,9 @@ import java.util.IdentityHashMap; } streamPeriodIndices.clear(); // Select tracks for each child, copying the resulting streams back into a new streams array. - SampleStream[] newStreams = new SampleStream[selections.length]; - SampleStream[] childStreams = new SampleStream[selections.length]; - TrackSelection[] childSelections = new TrackSelection[selections.length]; + @NullableType SampleStream[] newStreams = new SampleStream[selections.length]; + @NullableType SampleStream[] childStreams = new SampleStream[selections.length]; + @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length]; ArrayList enabledPeriodsList = new ArrayList<>(periods.length); for (int i = 0; i < periods.length; i++) { for (int j = 0; j < selections.length; j++) { @@ -114,10 +120,10 @@ import java.util.IdentityHashMap; for (int j = 0; j < selections.length; j++) { if (selectionChildIndices[j] == i) { // Assert that the child provided a stream for the selection. - Assertions.checkState(childStreams[j] != null); + SampleStream childStream = Assertions.checkNotNull(childStreams[j]); newStreams[j] = childStreams[j]; periodEnabled = true; - streamPeriodIndices.put(childStreams[j], i); + streamPeriodIndices.put(childStream, i); } else if (streamChildIndices[j] == i) { // Assert that the child cleared any previous stream. Assertions.checkState(childStreams[j] == null); @@ -163,6 +169,11 @@ import java.util.IdentityHashMap; } } + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); + } + @Override public long getNextLoadPositionUs() { return compositeSequenceableLoader.getNextLoadPositionUs(); @@ -208,7 +219,8 @@ import java.util.IdentityHashMap; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return enabledPeriods[0].getAdjustedSeekPositionUs(positionUs, seekParameters); + MediaPeriod queryPeriod = enabledPeriods.length > 0 ? enabledPeriods[0] : periods[0]; + return queryPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); } // MediaPeriod.Callback implementation @@ -233,12 +245,12 @@ import java.util.IdentityHashMap; } } trackGroups = new TrackGroupArray(trackGroupArray); - callback.onPrepared(this); + Assertions.checkNotNull(callback).onPrepared(this); } @Override public void onContinueLoadingRequested(MediaPeriod ignored) { - callback.onContinueLoadingRequested(this); + Assertions.checkNotNull(callback).onContinueLoadingRequested(this); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 6b1a362b5..dd7675f3d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -71,9 +71,8 @@ public final class MergingMediaSource extends CompositeMediaSource { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Object primaryManifest; private int periodCount; - private IllegalMergeException mergeError; + @Nullable private IllegalMergeException mergeError; /** * @param mediaSources The {@link MediaSource}s to merge. @@ -104,7 +103,7 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); for (int i = 0; i < mediaSources.length; i++) { prepareChildSource(i, mediaSources[i]); @@ -140,10 +139,9 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); Arrays.fill(timelines, null); - primaryManifest = null; periodCount = PERIOD_COUNT_UNSET; mergeError = null; pendingTimelineSources.clear(); @@ -152,7 +150,7 @@ public final class MergingMediaSource extends CompositeMediaSource { @Override protected void onChildSourceInfoRefreshed( - Integer id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { + Integer id, MediaSource mediaSource, Timeline timeline) { if (mergeError == null) { mergeError = checkTimelineMerges(timeline); } @@ -161,20 +159,19 @@ public final class MergingMediaSource extends CompositeMediaSource { } pendingTimelineSources.remove(mediaSource); timelines[id] = timeline; - if (mediaSource == mediaSources[0]) { - primaryManifest = manifest; - } if (pendingTimelineSources.isEmpty()) { - refreshSourceInfo(timelines[0], primaryManifest); + refreshSourceInfo(timelines[0]); } } @Override - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + @Nullable + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( Integer id, MediaPeriodId mediaPeriodId) { return id == 0 ? mediaPeriodId : null; } + @Nullable private IllegalMergeException checkTimelineMerges(Timeline timeline) { if (periodCount == PERIOD_COUNT_UNSET) { periodCount = timeline.getPeriodCount(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index f7f8a4506..277e17410 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -55,6 +56,9 @@ import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.checkerframework.checker.nullness.compatqual.NullableType; /** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */ @@ -71,13 +75,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; interface Listener { /** - * Called when the duration or ability to seek within the period changes. + * Called when the duration, the ability to seek within the period, or the categorization as + * live stream changes. * * @param durationUs The duration of the period, or {@link C#TIME_UNSET}. * @param isSeekable Whether the period is seekable. + * @param isLive Whether the period is live. */ - void onSourceInfoRefreshed(long durationUs, boolean isSeekable); - + void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive); } /** @@ -86,11 +91,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000; + private static final Map ICY_METADATA_HEADERS = createIcyMetadataHeaders(); + private static final Format ICY_FORMAT = Format.createSampleFormat("icy", MimeTypes.APPLICATION_ICY, Format.OFFSET_SAMPLE_RELATIVE); private final Uri uri; private final DataSource dataSource; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Listener listener; @@ -122,6 +130,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private int enabledTrackCount; private long durationUs; private long length; + private boolean isLive; private long lastSeekPositionUs; private long pendingResetPositionUs; @@ -153,6 +162,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Uri uri, DataSource dataSource, Extractor[] extractors, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Listener listener, @@ -161,6 +171,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.listener = listener; @@ -193,7 +204,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise // sampleQueues may still be being modified by the loading thread. for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.discardToEnd(); + sampleQueue.preRelease(); } } loader.release(/* callback= */ this); @@ -206,7 +217,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void onLoaderReleased() { for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(); + sampleQueue.release(); } extractorHolder.release(); } @@ -270,13 +281,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // If there's still a chance of avoiding a seek, try and seek within the sample queue. if (!seekRequired) { SampleQueue sampleQueue = sampleQueues[track]; - sampleQueue.rewind(); - // A seek can be avoided if we're able to advance to the current playback position in the + // A seek can be avoided if we're able to seek to the current playback position in the // sample queue, or if we haven't read anything from the queue since the previous seek // (this case is common for sparse tracks such as metadata tracks). In all other cases a // seek is required. - seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED - && sampleQueue.getReadIndex() != 0; + seekRequired = + !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true) + && sampleQueue.getReadIndex() != 0; } } } @@ -340,6 +351,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return continuedLoading; } + @Override + public boolean isLoading() { + return loader.isLoading() && loadCondition.isOpen(); + } + @Override public long getNextLoadPositionUs() { return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs(); @@ -437,24 +453,32 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // SampleStream methods. /* package */ boolean isReady(int track) { - return !suppressRead() && (loadingFinished || sampleQueues[track].hasNextSample()); + return !suppressRead() && sampleQueues[track].isReady(loadingFinished); + } + + /* package */ void maybeThrowError(int sampleQueueIndex) throws IOException { + sampleQueues[sampleQueueIndex].maybeThrowError(); + maybeThrowError(); } /* package */ void maybeThrowError() throws IOException { loader.maybeThrowError(loadErrorHandlingPolicy.getMinimumLoadableRetryCount(dataType)); } - /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, + /* package */ int readData( + int sampleQueueIndex, + FormatHolder formatHolder, + DecoderInputBuffer buffer, boolean formatRequired) { if (suppressRead()) { return C.RESULT_NOTHING_READ; } - maybeNotifyDownstreamFormat(track); + maybeNotifyDownstreamFormat(sampleQueueIndex); int result = - sampleQueues[track].read( + sampleQueues[sampleQueueIndex].read( formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_NOTHING_READ) { - maybeStartDeferredRetry(track); + maybeStartDeferredRetry(sampleQueueIndex); } return result; } @@ -469,10 +493,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { skipCount = sampleQueue.advanceToEnd(); } else { - skipCount = sampleQueue.advanceTo(positionUs, true, true); - if (skipCount == SampleQueue.ADVANCE_FAILED) { - skipCount = 0; - } + skipCount = sampleQueue.advanceTo(positionUs); } if (skipCount == 0) { maybeStartDeferredRetry(track); @@ -499,7 +520,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; boolean[] trackIsAudioVideoFlags = getPreparedState().trackIsAudioVideoFlags; if (!pendingDeferredRetry || !trackIsAudioVideoFlags[track] - || sampleQueues[track].hasNextSample()) { + || sampleQueues[track].isReady(/* loadingFinished= */ false)) { return; } pendingResetPositionUs = 0; @@ -527,7 +548,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; - listener.onSourceInfoRefreshed(durationUs, isSeekable); + listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive); } eventDispatcher.loadCompleted( loadable.dataSpec, @@ -658,7 +679,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return sampleQueues[i]; } } - SampleQueue trackOutput = new SampleQueue(allocator); + SampleQueue trackOutput = new SampleQueue( + allocator, /* playbackLooper= */ handler.getLooper(), drmSessionManager); trackOutput.setUpstreamFormatChangeListener(this); @NullableType TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1); @@ -708,16 +730,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; trackFormat = trackFormat.copyWithBitrate(icyHeaders.bitrate); } } + if (trackFormat.drmInitData != null) { + trackFormat = + trackFormat.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(trackFormat.drmInitData)); + } trackArray[i] = new TrackGroup(trackFormat); } - dataType = - length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET - ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE - : C.DATA_TYPE_MEDIA; + isLive = length == C.LENGTH_UNSET && seekMap.getDurationUs() == C.TIME_UNSET; + dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; preparedState = new PreparedState(seekMap, new TrackGroupArray(trackArray), trackIsAudioVideoFlags); prepared = true; - listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); + listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); Assertions.checkNotNull(callback).onPrepared(this); } @@ -818,9 +843,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; - sampleQueue.rewind(); - boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) - != SampleQueue.ADVANCE_FAILED; + boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is @@ -868,7 +891,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void maybeThrowError() throws IOException { - ProgressiveMediaPeriod.this.maybeThrowError(); + ProgressiveMediaPeriod.this.maybeThrowError(track); } @Override @@ -1006,9 +1029,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; position, C.LENGTH_UNSET, customCacheKey, - DataSpec.FLAG_ALLOW_ICY_METADATA - | DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN - | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); + DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION, + ICY_METADATA_HEADERS); } private void setLoadPosition(long position, long timeUs) { @@ -1024,7 +1046,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final Extractor[] extractors; - private @Nullable Extractor extractor; + @Nullable private Extractor extractor; /** * Creates a holder that will select an extractor and initialize it using the specified output. @@ -1135,4 +1157,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return 31 * id + (isIcyTrack ? 1 : 0); } } + + private static Map createIcyMetadataHeaders() { + Map headers = new HashMap<>(); + headers.put( + IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, + IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE); + return Collections.unmodifiableMap(headers); + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index f448b0b6c..b48e7835a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -18,10 +18,11 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; @@ -45,13 +46,14 @@ public final class ProgressiveMediaSource extends BaseMediaSource implements ProgressiveMediaPeriod.Listener { /** Factory for {@link ProgressiveMediaSource}s. */ - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; private ExtractorsFactory extractorsFactory; @Nullable private String customCacheKey; @Nullable private Object tag; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private int continueLoadingCheckIntervalBytes; private boolean isCreateCalled; @@ -75,6 +77,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource public Factory(DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } @@ -87,7 +90,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * possible formats are known, pass a factory that instantiates extractors for those * formats. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. * @deprecated Pass the {@link ExtractorsFactory} via {@link #Factory(DataSource.Factory, * ExtractorsFactory)}. This is necessary so that proguard can treat the default extractors * factory as unused. @@ -106,9 +109,9 @@ public final class ProgressiveMediaSource extends BaseMediaSource * @param customCacheKey A custom key that uniquely identifies the original stream. Used for * cache indexing. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ - public Factory setCustomCacheKey(String customCacheKey) { + public Factory setCustomCacheKey(@Nullable String customCacheKey) { Assertions.checkState(!isCreateCalled); this.customCacheKey = customCacheKey; return this; @@ -121,7 +124,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * * @param tag A tag for the media source. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ public Factory setTag(Object tag) { Assertions.checkState(!isCreateCalled); @@ -135,7 +138,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { Assertions.checkState(!isCreateCalled); @@ -152,7 +155,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource * each invocation of {@link * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. + * @throws IllegalStateException If {@link #createMediaSource(Uri)} has already been called. */ public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { Assertions.checkState(!isCreateCalled); @@ -160,6 +163,24 @@ public final class ProgressiveMediaSource extends BaseMediaSource return this; } + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + @Override + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); + return this; + } + /** * Returns a new {@link ProgressiveMediaSource} using the current parameters. * @@ -173,6 +194,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource uri, dataSourceFactory, extractorsFactory, + drmSessionManager, loadErrorHandlingPolicy, customCacheKey, continueLoadingCheckIntervalBytes, @@ -194,6 +216,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private final Uri uri; private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; @Nullable private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @@ -201,6 +224,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource private long timelineDurationUs; private boolean timelineIsSeekable; + private boolean timelineIsLive; @Nullable private TransferListener transferListener; // TODO: Make private when ExtractorMediaSource is deleted. @@ -208,6 +232,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes, @@ -215,6 +240,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; + this.drmSessionManager = drmSessionManager; this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; @@ -229,9 +255,10 @@ public final class ProgressiveMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; - notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); + drmSessionManager.prepare(); + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable, timelineIsLive); } @Override @@ -249,6 +276,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource uri, dataSource, extractorsFactory.createExtractors(), + drmSessionManager, loadableLoadErrorHandlingPolicy, createEventDispatcher(id), this, @@ -263,32 +291,40 @@ public final class ProgressiveMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { - // Do nothing. + protected void releaseSourceInternal() { + drmSessionManager.release(); } // ProgressiveMediaPeriod.Listener implementation. @Override - public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { + public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { // If we already have the duration from a previous source info refresh, use it. durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; - if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) { + if (timelineDurationUs == durationUs + && timelineIsSeekable == isSeekable + && timelineIsLive == isLive) { // Suppress no-op source info changes. return; } - notifySourceInfoRefreshed(durationUs, isSeekable); + notifySourceInfoRefreshed(durationUs, isSeekable, isLive); } // Internal methods. - private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { + private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. + timelineIsLive = isLive; + // TODO: Split up isDynamic into multiple fields to indicate which values may change. Then + // indicate that the duration may change until it's known. See [internal: b/69703223]. refreshSourceInfo( new SinglePeriodTimeline( - timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag), - /* manifest= */ null); + timelineDurationUs, + timelineIsSeekable, + /* isDynamic= */ false, + /* isLive= */ timelineIsLive, + /* manifest= */ null, + tag)); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java new file mode 100644 index 000000000..3779fe33e --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleDataQueue.java @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.CryptoInfo; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; +import com.google.android.exoplayer2.source.SampleQueue.SampleExtrasHolder; +import com.google.android.exoplayer2.upstream.Allocation; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** A queue of media sample data. */ +/* package */ class SampleDataQueue { + + private static final int INITIAL_SCRATCH_SIZE = 32; + + private final Allocator allocator; + private final int allocationLength; + private final ParsableByteArray scratch; + + // References into the linked list of allocations. + private AllocationNode firstAllocationNode; + private AllocationNode readAllocationNode; + private AllocationNode writeAllocationNode; + + // Accessed only by the loading thread (or the consuming thread when there is no loading thread). + private long totalBytesWritten; + + public SampleDataQueue(Allocator allocator) { + this.allocator = allocator; + allocationLength = allocator.getIndividualAllocationLength(); + scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); + firstAllocationNode = new AllocationNode(/* startPosition= */ 0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** Clears all sample data. */ + public void reset() { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(0, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + totalBytesWritten = 0; + allocator.trim(); + } + + /** + * Discards sample data bytes from the write side of the queue. + * + * @param totalBytesWritten The reduced total number of bytes written after the samples have been + * discarded, or 0 if the queue is now empty. + */ + public void discardUpstreamSampleBytes(long totalBytesWritten) { + this.totalBytesWritten = totalBytesWritten; + if (this.totalBytesWritten == 0 + || this.totalBytesWritten == firstAllocationNode.startPosition) { + clearAllocationNodes(firstAllocationNode); + firstAllocationNode = new AllocationNode(this.totalBytesWritten, allocationLength); + readAllocationNode = firstAllocationNode; + writeAllocationNode = firstAllocationNode; + } else { + // Find the last node containing at least 1 byte of data that we need to keep. + AllocationNode lastNodeToKeep = firstAllocationNode; + while (this.totalBytesWritten > lastNodeToKeep.endPosition) { + lastNodeToKeep = lastNodeToKeep.next; + } + // Discard all subsequent nodes. + AllocationNode firstNodeToDiscard = lastNodeToKeep.next; + clearAllocationNodes(firstNodeToDiscard); + // Reset the successor of the last node to be an uninitialized node. + lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); + // Update writeAllocationNode and readAllocationNode as necessary. + writeAllocationNode = + this.totalBytesWritten == lastNodeToKeep.endPosition + ? lastNodeToKeep.next + : lastNodeToKeep; + if (readAllocationNode == firstNodeToDiscard) { + readAllocationNode = lastNodeToKeep.next; + } + } + } + + // Called by the consuming thread. + + /** Rewinds the read position to the first sample in the queue. */ + public void rewind() { + readAllocationNode = firstAllocationNode; + } + + /** + * Reads data from the rolling buffer to populate a decoder input buffer. + * + * @param buffer The buffer to populate. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + public void readToBuffer(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + // Read encryption data if the sample is encrypted. + if (buffer.isEncrypted()) { + readEncryptionData(buffer, extrasHolder); + } + // Read sample data, extracting supplemental data into a separate buffer if needed. + if (buffer.hasSupplementalData()) { + // If there is supplemental data, the sample data is prefixed by its size. + scratch.reset(4); + readData(extrasHolder.offset, scratch.data, 4); + int sampleSize = scratch.readUnsignedIntToInt(); + extrasHolder.offset += 4; + extrasHolder.size -= 4; + + // Write the sample data. + buffer.ensureSpaceForWrite(sampleSize); + readData(extrasHolder.offset, buffer.data, sampleSize); + extrasHolder.offset += sampleSize; + extrasHolder.size -= sampleSize; + + // Write the remaining data as supplemental data. + buffer.resetSupplementalData(extrasHolder.size); + readData(extrasHolder.offset, buffer.supplementalData, extrasHolder.size); + } else { + // Write the sample data. + buffer.ensureSpaceForWrite(extrasHolder.size); + readData(extrasHolder.offset, buffer.data, extrasHolder.size); + } + } + + /** + * Advances the read position to the specified absolute position. + * + * @param absolutePosition The new absolute read position. May be {@link C#POSITION_UNSET}, in + * which case calling this method is a no-op. + */ + public void discardDownstreamTo(long absolutePosition) { + if (absolutePosition == C.POSITION_UNSET) { + return; + } + while (absolutePosition >= firstAllocationNode.endPosition) { + // Advance firstAllocationNode to the specified absolute position. Also clear nodes that are + // advanced past, and return their underlying allocations to the allocator. + allocator.release(firstAllocationNode.allocation); + firstAllocationNode = firstAllocationNode.clear(); + } + if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { + // We discarded the node referenced by readAllocationNode. We need to advance it to the first + // remaining node. + readAllocationNode = firstAllocationNode; + } + } + + // Called by the loading thread. + + public long getTotalBytesWritten() { + return totalBytesWritten; + } + + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + length = preAppend(length); + int bytesAppended = + input.read( + writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), + length); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + postAppend(bytesAppended); + return bytesAppended; + } + + public void sampleData(ParsableByteArray buffer, int length) { + while (length > 0) { + int bytesAppended = preAppend(length); + buffer.readBytes( + writeAllocationNode.allocation.data, + writeAllocationNode.translateOffset(totalBytesWritten), + bytesAppended); + length -= bytesAppended; + postAppend(bytesAppended); + } + } + + // Private methods. + + /** + * Reads encryption data for the current sample. + * + *

    The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and {@link + * SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The same + * value is added to {@link SampleExtrasHolder#offset}. + * + * @param buffer The buffer into which the encryption data should be written. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { + long offset = extrasHolder.offset; + + // Read the signal byte. + scratch.reset(1); + readData(offset, scratch.data, 1); + offset++; + byte signalByte = scratch.data[0]; + boolean subsampleEncryption = (signalByte & 0x80) != 0; + int ivSize = signalByte & 0x7F; + + // Read the initialization vector. + CryptoInfo cryptoInfo = buffer.cryptoInfo; + if (cryptoInfo.iv == null) { + cryptoInfo.iv = new byte[16]; + } else { + // Zero out cryptoInfo.iv so that if ivSize < 16, the remaining bytes are correctly set to 0. + Arrays.fill(cryptoInfo.iv, (byte) 0); + } + readData(offset, cryptoInfo.iv, ivSize); + offset += ivSize; + + // Read the subsample count, if present. + int subsampleCount; + if (subsampleEncryption) { + scratch.reset(2); + readData(offset, scratch.data, 2); + offset += 2; + subsampleCount = scratch.readUnsignedShort(); + } else { + subsampleCount = 1; + } + + // Write the clear and encrypted subsample sizes. + @Nullable int[] clearDataSizes = cryptoInfo.numBytesOfClearData; + if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { + clearDataSizes = new int[subsampleCount]; + } + @Nullable int[] encryptedDataSizes = cryptoInfo.numBytesOfEncryptedData; + if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { + encryptedDataSizes = new int[subsampleCount]; + } + if (subsampleEncryption) { + int subsampleDataLength = 6 * subsampleCount; + scratch.reset(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] = extrasHolder.size - (int) (offset - extrasHolder.offset); + } + + // Populate the cryptoInfo. + CryptoData cryptoData = extrasHolder.cryptoData; + cryptoInfo.set( + subsampleCount, + clearDataSizes, + encryptedDataSizes, + cryptoData.encryptionKey, + cryptoInfo.iv, + cryptoData.cryptoMode, + cryptoData.encryptedBlocks, + cryptoData.clearBlocks); + + // Adjust the offset and size to take into account the bytes read. + int bytesRead = (int) (offset - extrasHolder.offset); + extrasHolder.offset += bytesRead; + extrasHolder.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) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * 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. + */ + private void readData(long absolutePosition, byte[] target, int length) { + advanceReadTo(absolutePosition); + int remaining = length; + while (remaining > 0) { + int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); + Allocation allocation = readAllocationNode.allocation; + System.arraycopy( + allocation.data, + readAllocationNode.translateOffset(absolutePosition), + target, + length - remaining, + toCopy); + remaining -= toCopy; + absolutePosition += toCopy; + if (absolutePosition == readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + } + + /** + * Advances the read position to the specified absolute position. + * + * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. + */ + private void advanceReadTo(long absolutePosition) { + while (absolutePosition >= readAllocationNode.endPosition) { + readAllocationNode = readAllocationNode.next; + } + } + + /** + * Clears allocation nodes starting from {@code fromNode}. + * + * @param fromNode The node from which to clear. + */ + private void clearAllocationNodes(AllocationNode fromNode) { + if (!fromNode.wasInitialized) { + return; + } + // Bulk release allocations for performance (it's significantly faster when using + // DefaultAllocator because the allocator's lock only needs to be acquired and released once) + // [Internal: See b/29542039]. + int allocationCount = + (writeAllocationNode.wasInitialized ? 1 : 0) + + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) + / allocationLength); + Allocation[] allocationsToRelease = new Allocation[allocationCount]; + AllocationNode currentNode = fromNode; + for (int i = 0; i < allocationsToRelease.length; i++) { + allocationsToRelease[i] = currentNode.allocation; + currentNode = currentNode.clear(); + } + allocator.release(allocationsToRelease); + } + + /** + * Called before writing sample data to {@link #writeAllocationNode}. May cause {@link + * #writeAllocationNode} to be initialized. + * + * @param length The number of bytes that the caller wishes to write. + * @return The number of bytes that the caller is permitted to write, which may be less than + * {@code length}. + */ + private int preAppend(int length) { + if (!writeAllocationNode.wasInitialized) { + writeAllocationNode.initialize( + allocator.allocate(), + new AllocationNode(writeAllocationNode.endPosition, allocationLength)); + } + return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); + } + + /** + * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. + * + * @param length The number of bytes that were written. + */ + private void postAppend(int length) { + totalBytesWritten += length; + if (totalBytesWritten == writeAllocationNode.endPosition) { + writeAllocationNode = writeAllocationNode.next; + } + } + + /** A node in a linked list of {@link Allocation}s held by the output. */ + private static final class AllocationNode { + + /** The absolute position of the start of the data (inclusive). */ + public final long startPosition; + /** The absolute position of the end of the data (exclusive). */ + public final long endPosition; + /** Whether the node has been initialized. Remains true after {@link #clear()}. */ + public boolean wasInitialized; + /** The {@link Allocation}, or {@code null} if the node is not initialized. */ + @Nullable public Allocation allocation; + /** + * The next {@link AllocationNode} in the list, or {@code null} if the node has not been + * initialized. Remains set after {@link #clear()}. + */ + @Nullable public AllocationNode next; + + /** + * @param startPosition See {@link #startPosition}. + * @param allocationLength The length of the {@link Allocation} with which this node will be + * initialized. + */ + public AllocationNode(long startPosition, int allocationLength) { + this.startPosition = startPosition; + this.endPosition = startPosition + allocationLength; + } + + /** + * Initializes the node. + * + * @param allocation The node's {@link Allocation}. + * @param next The next {@link AllocationNode}. + */ + public void initialize(Allocation allocation, AllocationNode next) { + this.allocation = allocation; + this.next = next; + wasInitialized = true; + } + + /** + * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to + * the specified absolute position. + * + * @param absolutePosition The absolute position. + * @return The corresponding offset into the allocation's data. + */ + public int translateOffset(long absolutePosition) { + return (int) (absolutePosition - startPosition) + allocation.offset; + } + + /** + * Clears {@link #allocation} and {@link #next}. + * + * @return The cleared next {@link AllocationNode}. + */ + public AllocationNode clear() { + allocation = null; + AllocationNode temp = next; + next = null; + return temp; + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java deleted file mode 100644 index 25cc73d4a..000000000 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ /dev/null @@ -1,591 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.extractor.TrackOutput.CryptoData; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; - -/** - * A queue of metadata describing the contents of a media buffer. - */ -/* package */ final class SampleMetadataQueue { - - /** - * A holder for sample metadata not held by {@link DecoderInputBuffer}. - */ - public static final class SampleExtrasHolder { - - public int size; - public long offset; - public CryptoData cryptoData; - - } - - private static final int SAMPLE_CAPACITY_INCREMENT = 1000; - - private int capacity; - private int[] sourceIds; - private long[] offsets; - private int[] sizes; - private int[] flags; - private long[] timesUs; - private CryptoData[] cryptoDatas; - private Format[] formats; - - private int length; - private int absoluteFirstIndex; - private int relativeFirstIndex; - private int readPosition; - - private long largestDiscardedTimestampUs; - private long largestQueuedTimestampUs; - private boolean isLastSampleQueued; - private boolean upstreamKeyframeRequired; - private boolean upstreamFormatRequired; - private Format upstreamFormat; - private int upstreamSourceId; - - public SampleMetadataQueue() { - capacity = SAMPLE_CAPACITY_INCREMENT; - sourceIds = new int[capacity]; - offsets = new long[capacity]; - timesUs = new long[capacity]; - flags = new int[capacity]; - sizes = new int[capacity]; - cryptoDatas = new CryptoData[capacity]; - formats = new Format[capacity]; - largestDiscardedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - upstreamFormatRequired = true; - upstreamKeyframeRequired = true; - } - - /** - * Clears all sample metadata from the queue. - * - * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, - * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) - * are assumed to have the current upstream format. If set to true, {@link #format(Format)} - * must be called after the reset before any more samples can be queued. - */ - public void reset(boolean resetUpstreamFormat) { - length = 0; - absoluteFirstIndex = 0; - relativeFirstIndex = 0; - readPosition = 0; - upstreamKeyframeRequired = true; - largestDiscardedTimestampUs = Long.MIN_VALUE; - largestQueuedTimestampUs = Long.MIN_VALUE; - isLastSampleQueued = false; - if (resetUpstreamFormat) { - upstreamFormat = null; - upstreamFormatRequired = true; - } - } - - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return absoluteFirstIndex + length; - } - - /** - * Discards samples from the write side of the queue. - * - * @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, or 0 - * if the queue is now empty. - */ - public long discardUpstreamSamples(int discardFromIndex) { - int discardCount = getWriteIndex() - discardFromIndex; - Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); - length -= discardCount; - largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); - isLastSampleQueued = discardCount == 0 && isLastSampleQueued; - if (length == 0) { - return 0; - } else { - int relativeLastWriteIndex = getRelativeIndex(length - 1); - return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; - } - } - - public void sourceId(int sourceId) { - upstreamSourceId = sourceId; - } - - // Called by the consuming thread. - - /** - * Returns the current absolute start index. - */ - public int getFirstIndex() { - return absoluteFirstIndex; - } - - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return absoluteFirstIndex + readPosition; - } - - /** - * Peeks the source id of the next sample to be read, or the current upstream source id if the - * queue is empty or if the read position is at the end of the queue. - * - * @return The source id. - */ - public int peekSourceId() { - int relativeReadIndex = getRelativeIndex(readPosition); - return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; - } - - /** - * Returns whether a sample is available to be read. - */ - public synchronized boolean hasNextSample() { - return readPosition != length; - } - - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public synchronized Format getUpstreamFormat() { - return upstreamFormatRequired ? null : upstreamFormat; - } - - /** - * Returns the largest sample timestamp that has been queued since the last call to - * {@link #reset(boolean)}. - *

    - * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - * - * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no - * samples have been queued. - */ - public synchronized long getLargestQueuedTimestampUs() { - return largestQueuedTimestampUs; - } - - /** - * Returns whether the last sample of the stream has knowingly been queued. A return value of - * {@code false} means that the last sample had not been queued or that it's unknown whether the - * last sample has been queued. - * - *

    Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not - * considered as having been queued. Samples that were dequeued from the front of the queue are - * considered as having been queued. - */ - public synchronized boolean isLastSampleQueued() { - return isLastSampleQueued; - } - - /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ - public synchronized long getFirstTimestampUs() { - return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; - } - - /** - * Rewinds the read position to the first sample retained in the queue. - */ - public synchronized void rewind() { - readPosition = 0; - } - - /** - * Attempts to read from the queue. - * - * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. - * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the - * end of the stream. If a sample is read then the buffer is populated with information about - * the sample, but not its data. The size and absolute position of the data in the rolling - * buffer is stored in {@code extrasHolder}, along with an encryption id if present and the - * absolute position of the first byte that may still be required after the current sample has - * been read. If a {@link DecoderInputBuffer#isFlagsOnly() flags-only} buffer is passed, only - * the buffer flags may be populated by this method and the read position of the queue will - * not change. May be null if the caller requires that the format of the stream be read even - * if it's not changing. - * @param formatRequired Whether the caller requires that the format of the stream be read even if - * it's not changing. A sample will never be read if set to true, however it is still possible - * for the end of stream or nothing to be read. - * @param loadingFinished True if an empty queue should be considered the end of the stream. - * @param downstreamFormat The current downstream {@link Format}. If the format of the next sample - * is different to the current downstream format then a format will be read. - * @param extrasHolder The holder into which extra sample information should be written. - * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or - * {@link C#RESULT_BUFFER_READ}. - */ - @SuppressWarnings("ReferenceEquality") - public synchronized int read( - FormatHolder formatHolder, - DecoderInputBuffer buffer, - boolean formatRequired, - boolean loadingFinished, - Format downstreamFormat, - SampleExtrasHolder extrasHolder) { - if (!hasNextSample()) { - if (loadingFinished || isLastSampleQueued) { - buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } else if (upstreamFormat != null - && (formatRequired || upstreamFormat != downstreamFormat)) { - formatHolder.format = upstreamFormat; - return C.RESULT_FORMAT_READ; - } else { - return C.RESULT_NOTHING_READ; - } - } - - int relativeReadIndex = getRelativeIndex(readPosition); - if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { - formatHolder.format = formats[relativeReadIndex]; - return C.RESULT_FORMAT_READ; - } - - buffer.setFlags(flags[relativeReadIndex]); - buffer.timeUs = timesUs[relativeReadIndex]; - if (buffer.isFlagsOnly()) { - return C.RESULT_BUFFER_READ; - } - - extrasHolder.size = sizes[relativeReadIndex]; - extrasHolder.offset = offsets[relativeReadIndex]; - extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; - - readPosition++; - return C.RESULT_BUFFER_READ; - } - - /** - * Attempts to advance the read position to the sample before or at the specified time. - * - * @param timeUs The time to advance to. - * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified - * time, rather than to any sample before or at that time. - * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the - * end of the queue, by advancing the read position to the last sample (or keyframe) in the - * queue. - * @return The number of samples that were skipped if the operation was successful, which may be - * equal to 0, or {@link SampleQueue#ADVANCE_FAILED} if the operation was not successful. A - * successful advance is one in which the read position was unchanged or advanced, and is now - * at a sample meeting the specified criteria. - */ - public synchronized int advanceTo(long timeUs, boolean toKeyframe, - boolean allowTimeBeyondBuffer) { - int relativeReadIndex = getRelativeIndex(readPosition); - if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] - || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { - return SampleQueue.ADVANCE_FAILED; - } - int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); - if (offset == -1) { - return SampleQueue.ADVANCE_FAILED; - } - readPosition += offset; - return offset; - } - - /** - * Advances the read position to the end of the queue. - * - * @return The number of samples that were skipped. - */ - public synchronized int advanceToEnd() { - int skipCount = length - readPosition; - readPosition = length; - return skipCount; - } - - /** - * Attempts to set the read position to the specified sample index. - * - * @param sampleIndex The sample index. - * @return Whether the read position was set successfully. False is returned if the specified - * index is smaller than the index of the first sample in the queue, or larger than the index - * of the next sample that will be written. - */ - public synchronized boolean setReadPosition(int sampleIndex) { - if (absoluteFirstIndex <= sampleIndex && sampleIndex <= absoluteFirstIndex + length) { - readPosition = sampleIndex - absoluteFirstIndex; - return true; - } - return false; - } - - /** - * Discards up to but not including the sample immediately before or at the specified time. - * - * @param timeUs The time to discard up to. - * @param toKeyframe If true then discards samples up to the keyframe before or at the specified - * time, rather than just any sample before or at that time. - * @param stopAtReadPosition If true then samples are only discarded if they're before the read - * position. If false then samples at and beyond the read position may be discarded, in which - * case the read position is advanced to the first remaining sample. - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { - if (length == 0 || timeUs < timesUs[relativeFirstIndex]) { - return C.POSITION_UNSET; - } - int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; - int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe); - if (discardCount == -1) { - return C.POSITION_UNSET; - } - return discardSamples(discardCount); - } - - /** - * Discards samples up to but not including the read position. - * - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardToRead() { - if (readPosition == 0) { - return C.POSITION_UNSET; - } - return discardSamples(readPosition); - } - - /** - * Discards all samples in the queue. The read position is also advanced. - * - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. - */ - public synchronized long discardToEnd() { - if (length == 0) { - return C.POSITION_UNSET; - } - return discardSamples(length); - } - - // Called by the loading thread. - - public synchronized boolean format(Format format) { - if (format == null) { - upstreamFormatRequired = true; - return false; - } - upstreamFormatRequired = false; - if (Util.areEqual(format, upstreamFormat)) { - // Suppress changes between equal formats so we can use referential equality in readData. - return false; - } else { - upstreamFormat = format; - return true; - } - } - - public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset, - int size, CryptoData cryptoData) { - if (upstreamKeyframeRequired) { - if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { - return; - } - upstreamKeyframeRequired = false; - } - Assertions.checkState(!upstreamFormatRequired); - - isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); - - int relativeEndIndex = getRelativeIndex(length); - timesUs[relativeEndIndex] = timeUs; - offsets[relativeEndIndex] = offset; - sizes[relativeEndIndex] = size; - flags[relativeEndIndex] = sampleFlags; - cryptoDatas[relativeEndIndex] = cryptoData; - formats[relativeEndIndex] = upstreamFormat; - sourceIds[relativeEndIndex] = upstreamSourceId; - - length++; - if (length == capacity) { - // Increase the capacity. - int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; - int[] newSourceIds = new int[newCapacity]; - long[] newOffsets = new long[newCapacity]; - long[] newTimesUs = new long[newCapacity]; - int[] newFlags = new int[newCapacity]; - int[] newSizes = new int[newCapacity]; - CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; - Format[] newFormats = new Format[newCapacity]; - int beforeWrap = capacity - relativeFirstIndex; - System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); - System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); - System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); - System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); - System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); - System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); - System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); - int afterWrap = relativeFirstIndex; - 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(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); - System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); - System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); - offsets = newOffsets; - timesUs = newTimesUs; - flags = newFlags; - sizes = newSizes; - cryptoDatas = newCryptoDatas; - formats = newFormats; - sourceIds = newSourceIds; - relativeFirstIndex = 0; - length = capacity; - capacity = newCapacity; - } - } - - /** - * Attempts to discard samples from the end of the queue to allow samples starting from the - * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. - * - * @param timeUs The timestamp at which the splice occurs. - * @return Whether the splice was successful. - */ - public synchronized boolean attemptSplice(long timeUs) { - if (length == 0) { - return timeUs > largestDiscardedTimestampUs; - } - long largestReadTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(readPosition)); - if (largestReadTimestampUs >= timeUs) { - return false; - } - int retainCount = length; - int relativeSampleIndex = getRelativeIndex(length - 1); - while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { - retainCount--; - relativeSampleIndex--; - if (relativeSampleIndex == -1) { - relativeSampleIndex = capacity - 1; - } - } - discardUpstreamSamples(absoluteFirstIndex + retainCount); - return true; - } - - // Internal methods. - - /** - * Finds the sample in the specified range that's before or at the specified time. If - * {@code keyframe} is {@code true} then the sample is additionally required to be a keyframe. - * - * @param relativeStartIndex The relative index from which to start searching. - * @param length The length of the range being searched. - * @param timeUs The specified time. - * @param keyframe Whether only keyframes should be considered. - * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching - * sample was found. - */ - private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { - // This could be optimized to use a binary search, however in practice callers to this method - // normally pass times near to the start of the search region. Hence it's unclear whether - // switching to a binary search would yield any real benefit. - int sampleCountToTarget = -1; - int searchIndex = relativeStartIndex; - for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { - if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - // We've found a suitable sample. - sampleCountToTarget = i; - } - searchIndex++; - if (searchIndex == capacity) { - searchIndex = 0; - } - } - return sampleCountToTarget; - } - - /** - * Discards the specified number of samples. - * - * @param discardCount The number of samples to discard. - * @return The corresponding offset up to which data should be discarded. - */ - private long discardSamples(int discardCount) { - largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, - getLargestTimestamp(discardCount)); - length -= discardCount; - absoluteFirstIndex += discardCount; - relativeFirstIndex += discardCount; - if (relativeFirstIndex >= capacity) { - relativeFirstIndex -= capacity; - } - readPosition -= discardCount; - if (readPosition < 0) { - readPosition = 0; - } - if (length == 0) { - int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; - return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; - } else { - return offsets[relativeFirstIndex]; - } - } - - /** - * Finds the largest timestamp of any sample from the start of the queue up to the specified - * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of - * the keyframe itself, and of subsequent frames. - * - * @param length The length of the range being searched. - * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. - */ - private long getLargestTimestamp(int length) { - if (length == 0) { - return Long.MIN_VALUE; - } - long largestTimestampUs = Long.MIN_VALUE; - int relativeSampleIndex = getRelativeIndex(length - 1); - for (int i = 0; i < length; i++) { - largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); - if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { - break; - } - relativeSampleIndex--; - if (relativeSampleIndex == -1) { - relativeSampleIndex = capacity - 1; - } - } - return largestTimestampUs; - } - - /** - * Returns the relative index for a given offset from the start of the queue. - * - * @param offset The offset, which must be in the range [0, length]. - */ - private int getRelativeIndex(int offset) { - int relativeIndex = relativeFirstIndex + offset; - return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; - } - -} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index e8f495343..c63b755f4 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,27 +15,30 @@ */ package com.google.android.exoplayer2.source; +import android.os.Looper; +import androidx.annotation.CallSuper; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.source.SampleMetadataQueue.SampleExtrasHolder; -import com.google.android.exoplayer2.upstream.Allocation; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; -import java.io.EOFException; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.nio.ByteBuffer; /** A queue of media samples. */ public class SampleQueue implements TrackOutput { - /** - * A listener for changes to the upstream format. - */ + /** A listener for changes to the upstream format. */ public interface UpstreamFormatChangedListener { /** @@ -44,74 +47,113 @@ public class SampleQueue implements TrackOutput { * @param format The new upstream format. */ void onUpstreamFormatChanged(Format format); - } - public static final int ADVANCE_FAILED = -1; + @VisibleForTesting /* package */ static final int SAMPLE_CAPACITY_INCREMENT = 1000; - private static final int INITIAL_SCRATCH_SIZE = 32; - - private final Allocator allocator; - private final int allocationLength; - private final SampleMetadataQueue metadataQueue; + private final SampleDataQueue sampleDataQueue; private final SampleExtrasHolder extrasHolder; - private final ParsableByteArray scratch; - - // References into the linked list of allocations. - private AllocationNode firstAllocationNode; - private AllocationNode readAllocationNode; - private AllocationNode writeAllocationNode; - - // Accessed only by the consuming thread. - private Format downstreamFormat; - - // Accessed only by the loading thread (or the consuming thread when there is no loading thread). - private boolean pendingFormatAdjustment; - private Format lastUnadjustedFormat; - private long sampleOffsetUs; - private long totalBytesWritten; - private boolean pendingSplice; + private final DrmSessionManager drmSessionManager; private UpstreamFormatChangedListener upstreamFormatChangeListener; + private final Looper playbackLooper; + + @Nullable private Format downstreamFormat; + @Nullable private DrmSession currentDrmSession; + + private int capacity; + private int[] sourceIds; + private long[] offsets; + private int[] sizes; + private int[] flags; + private long[] timesUs; + private CryptoData[] cryptoDatas; + private Format[] formats; + + private int length; + private int absoluteFirstIndex; + private int relativeFirstIndex; + private int readPosition; + + private long largestDiscardedTimestampUs; + private long largestQueuedTimestampUs; + private boolean isLastSampleQueued; + private boolean upstreamKeyframeRequired; + private boolean upstreamFormatRequired; + private Format upstreamFormat; + private Format upstreamCommittedFormat; + private int upstreamSourceId; + + private boolean pendingUpstreamFormatAdjustment; + private Format unadjustedUpstreamFormat; + private long sampleOffsetUs; + private boolean pendingSplice; /** + * Creates a sample queue. + * * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. + * @param playbackLooper The looper associated with the media playback thread. + * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} + * from. The created instance does not take ownership of this {@link DrmSessionManager}. */ - public SampleQueue(Allocator allocator) { - this.allocator = allocator; - allocationLength = allocator.getIndividualAllocationLength(); - metadataQueue = new SampleMetadataQueue(); + public SampleQueue(Allocator allocator, Looper playbackLooper, DrmSessionManager drmSessionManager) { + sampleDataQueue = new SampleDataQueue(allocator); + this.playbackLooper = playbackLooper; + this.drmSessionManager = drmSessionManager; extrasHolder = new SampleExtrasHolder(); - scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); - firstAllocationNode = new AllocationNode(0, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; + capacity = SAMPLE_CAPACITY_INCREMENT; + sourceIds = new int[capacity]; + offsets = new long[capacity]; + timesUs = new long[capacity]; + flags = new int[capacity]; + sizes = new int[capacity]; + cryptoDatas = new CryptoData[capacity]; + formats = new Format[capacity]; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + upstreamFormatRequired = true; + upstreamKeyframeRequired = true; } - // Called by the consuming thread, but only when there is no loading thread. + // Called by the consuming thread when there is no loading thread. - /** - * Resets the output without clearing the upstream format. Equivalent to {@code reset(false)}. - */ - public void reset() { - reset(false); + /** Calls {@link #reset(boolean) reset(true)} and releases any resources owned by the queue. */ + @CallSuper + public void release() { + reset(/* resetUpstreamFormat= */ true); + releaseDrmSessionReferences(); + } + + /** Convenience method for {@code reset(false)}. */ + public final void reset() { + reset(/* resetUpstreamFormat= */ false); } /** - * Resets the output. + * Clears all samples from the queue. * * @param resetUpstreamFormat Whether the upstream format should be cleared. If set to false, * samples queued after the reset (and before a subsequent call to {@link #format(Format)}) * are assumed to have the current upstream format. If set to true, {@link #format(Format)} * must be called after the reset before any more samples can be queued. */ + @CallSuper public void reset(boolean resetUpstreamFormat) { - metadataQueue.reset(resetUpstreamFormat); - clearAllocationNodes(firstAllocationNode); - firstAllocationNode = new AllocationNode(0, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; - totalBytesWritten = 0; - allocator.trim(); + sampleDataQueue.reset(); + length = 0; + absoluteFirstIndex = 0; + relativeFirstIndex = 0; + readPosition = 0; + upstreamKeyframeRequired = true; + largestDiscardedTimestampUs = Long.MIN_VALUE; + largestQueuedTimestampUs = Long.MIN_VALUE; + isLastSampleQueued = false; + upstreamCommittedFormat = null; + if (resetUpstreamFormat) { + unadjustedUpstreamFormat = null; + upstreamFormat = null; + upstreamFormatRequired = true; + } } /** @@ -119,22 +161,18 @@ public class SampleQueue implements TrackOutput { * * @param sourceId The source identifier. */ - public void sourceId(int sourceId) { - metadataQueue.sourceId(sourceId); + public final void sourceId(int sourceId) { + upstreamSourceId = sourceId; } - /** - * Indicates samples that are subsequently queued should be spliced into those already queued. - */ - public void splice() { + /** Indicates samples that are subsequently queued should be spliced into those already queued. */ + public final void splice() { pendingSplice = true; } - /** - * Returns the current absolute write index. - */ - public int getWriteIndex() { - return metadataQueue.getWriteIndex(); + /** Returns the current absolute write index. */ + public final int getWriteIndex() { + return absoluteFirstIndex + length; } /** @@ -143,54 +181,40 @@ public class SampleQueue implements TrackOutput { * @param discardFromIndex The absolute index of the first sample to be discarded. Must be in the * range [{@link #getReadIndex()}, {@link #getWriteIndex()}]. */ - public void discardUpstreamSamples(int discardFromIndex) { - totalBytesWritten = metadataQueue.discardUpstreamSamples(discardFromIndex); - if (totalBytesWritten == 0 || totalBytesWritten == firstAllocationNode.startPosition) { - clearAllocationNodes(firstAllocationNode); - firstAllocationNode = new AllocationNode(totalBytesWritten, allocationLength); - readAllocationNode = firstAllocationNode; - writeAllocationNode = firstAllocationNode; - } else { - // Find the last node containing at least 1 byte of data that we need to keep. - AllocationNode lastNodeToKeep = firstAllocationNode; - while (totalBytesWritten > lastNodeToKeep.endPosition) { - lastNodeToKeep = lastNodeToKeep.next; - } - // Discard all subsequent nodes. - AllocationNode firstNodeToDiscard = lastNodeToKeep.next; - clearAllocationNodes(firstNodeToDiscard); - // Reset the successor of the last node to be an uninitialized node. - lastNodeToKeep.next = new AllocationNode(lastNodeToKeep.endPosition, allocationLength); - // Update writeAllocationNode and readAllocationNode as necessary. - writeAllocationNode = totalBytesWritten == lastNodeToKeep.endPosition ? lastNodeToKeep.next - : lastNodeToKeep; - if (readAllocationNode == firstNodeToDiscard) { - readAllocationNode = lastNodeToKeep.next; - } - } + public final void discardUpstreamSamples(int discardFromIndex) { + sampleDataQueue.discardUpstreamSampleBytes(discardUpstreamSampleMetadata(discardFromIndex)); } // Called by the consuming thread. - /** - * Returns whether a sample is available to be read. - */ - public boolean hasNextSample() { - return metadataQueue.hasNextSample(); + /** Calls {@link #discardToEnd()} and releases any resources owned by the queue. */ + @CallSuper + public void preRelease() { + discardToEnd(); + releaseDrmSessionReferences(); } /** - * Returns the absolute index of the first sample. + * Throws an error that's preventing data from being read. Does nothing if no such error exists. + * + * @throws IOException The underlying error. */ - public int getFirstIndex() { - return metadataQueue.getFirstIndex(); + @CallSuper + public void maybeThrowError() throws IOException { + // TODO: Avoid throwing if the DRM error is not preventing a read operation. + if (currentDrmSession != null && currentDrmSession.getState() == DrmSession.STATE_ERROR) { + throw Assertions.checkNotNull(currentDrmSession.getError()); + } } - /** - * Returns the current absolute read index. - */ - public int getReadIndex() { - return metadataQueue.getReadIndex(); + /** Returns the current absolute start index. */ + public final int getFirstIndex() { + return absoluteFirstIndex; + } + + /** Returns the current absolute read index. */ + public final int getReadIndex() { + return absoluteFirstIndex + readPosition; } /** @@ -199,122 +223,87 @@ public class SampleQueue implements TrackOutput { * * @return The source id. */ - public int peekSourceId() { - return metadataQueue.peekSourceId(); + public final synchronized int peekSourceId() { + int relativeReadIndex = getRelativeIndex(readPosition); + return hasNextSample() ? sourceIds[relativeReadIndex] : upstreamSourceId; } - /** - * Returns the upstream {@link Format} in which samples are being queued. - */ - public Format getUpstreamFormat() { - return metadataQueue.getUpstreamFormat(); + /** Returns the upstream {@link Format} in which samples are being queued. */ + public final synchronized Format getUpstreamFormat() { + return upstreamFormatRequired ? null : upstreamFormat; } /** * Returns the largest sample timestamp that has been queued since the last {@link #reset}. - *

    - * Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * + *

    Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not * considered as having been queued. Samples that were dequeued from the front of the queue are * considered as having been queued. * * @return The largest sample timestamp that has been queued, or {@link Long#MIN_VALUE} if no * samples have been queued. */ - public long getLargestQueuedTimestampUs() { - return metadataQueue.getLargestQueuedTimestampUs(); + public final synchronized long getLargestQueuedTimestampUs() { + return largestQueuedTimestampUs; } /** * Returns whether the last sample of the stream has knowingly been queued. A return value of * {@code false} means that the last sample had not been queued or that it's unknown whether the * last sample has been queued. + * + *

    Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. */ - public boolean isLastSampleQueued() { - return metadataQueue.isLastSampleQueued(); + public final synchronized boolean isLastSampleQueued() { + return isLastSampleQueued; } /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ - public long getFirstTimestampUs() { - return metadataQueue.getFirstTimestampUs(); + public final synchronized long getFirstTimestampUs() { + return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; } /** - * Rewinds the read position to the first sample in the queue. - */ - public void rewind() { - metadataQueue.rewind(); - readAllocationNode = firstAllocationNode; - } - - /** - * Discards up to but not including the sample immediately before or at the specified time. + * Returns whether there is data available for reading. * - * @param timeUs The time to discard to. - * @param toKeyframe If true then discards samples up to the keyframe before or at the specified - * time, rather than any sample before or at that time. - * @param stopAtReadPosition If true then samples are only discarded if they're before the - * read position. If false then samples at and beyond the read position may be discarded, in - * which case the read position is advanced to the first remaining sample. - */ - public void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { - discardDownstreamTo(metadataQueue.discardTo(timeUs, toKeyframe, stopAtReadPosition)); - } - - /** - * Discards up to but not including the read position. - */ - public void discardToRead() { - discardDownstreamTo(metadataQueue.discardToRead()); - } - - /** - * Discards to the end of the queue. The read position is also advanced. - */ - public void discardToEnd() { - discardDownstreamTo(metadataQueue.discardToEnd()); - } - - /** - * Advances the read position to the end of the queue. + *

    Note: If the stream has ended then a buffer with the end of stream flag can always be read + * from {@link #read}. Hence an ended stream is always ready. * - * @return The number of samples that were skipped. + * @param loadingFinished Whether no more samples will be written to the sample queue. When true, + * this method returns true if the sample queue is empty, because an empty sample queue means + * the end of stream has been reached. When false, this method returns false if the sample + * queue is empty. */ - public int advanceToEnd() { - return metadataQueue.advanceToEnd(); - } - - /** - * Attempts to advance the read position to the sample before or at the specified time. - * - * @param timeUs The time to advance to. - * @param toKeyframe If true then attempts to advance to the keyframe before or at the specified - * time, rather than to any sample before or at that time. - * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the - * end of the queue, by advancing the read position to the last sample (or keyframe). - * @return The number of samples that were skipped if the operation was successful, which may be - * equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful - * advance is one in which the read position was unchanged or advanced, and is now at a sample - * meeting the specified criteria. - */ - public int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { - return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); - } - - /** - * Attempts to set the read position to the specified sample index. - * - * @param sampleIndex The sample index. - * @return Whether the read position was set successfully. False is returned if the specified - * index is smaller than the index of the first sample in the queue, or larger than the index - * of the next sample that will be written. - */ - public boolean setReadPosition(int sampleIndex) { - return metadataQueue.setReadPosition(sampleIndex); + @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat + @CallSuper + public synchronized boolean isReady(boolean loadingFinished) { + if (!hasNextSample()) { + return loadingFinished + || isLastSampleQueued + || (upstreamFormat != null && upstreamFormat != downstreamFormat); + } + int relativeReadIndex = getRelativeIndex(readPosition); + if (formats[relativeReadIndex] != downstreamFormat) { + // A format can be read. + return true; + } + return mayReadSample(relativeReadIndex); } /** * Attempts to read from the queue. * + *

    {@link Format Formats} read from this method may be associated to a {@link DrmSession} + * through {@link FormatHolder#drmSession}, which is populated in two scenarios: + * + *

      + *
    • The {@link Format} has a non-null {@link Format#drmInitData}. + *
    • The {@link DrmSessionManager} provides placeholder sessions for this queue's track type. + * See {@link DrmSessionManager#acquirePlaceholderSession(Looper, int)}. + *
    + * * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * end of the stream. If the end of the stream has been reached, the {@link @@ -330,421 +319,610 @@ public class SampleQueue implements TrackOutput { * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * {@link C#RESULT_BUFFER_READ}. */ + @CallSuper public int read( FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired, boolean loadingFinished, long decodeOnlyUntilUs) { - int result = metadataQueue.read(formatHolder, buffer, formatRequired, loadingFinished, - downstreamFormat, extrasHolder); - switch (result) { - case C.RESULT_FORMAT_READ: - downstreamFormat = formatHolder.format; - return C.RESULT_FORMAT_READ; - case C.RESULT_BUFFER_READ: - if (!buffer.isEndOfStream()) { - if (buffer.timeUs < decodeOnlyUntilUs) { - buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); - } - if (!buffer.isFlagsOnly()) { - // Read encryption data if the sample is encrypted. - if (buffer.isEncrypted()) { - readEncryptionData(buffer, extrasHolder); - } - // Write the sample data into the holder. - buffer.ensureSpaceForWrite(extrasHolder.size); - readData(extrasHolder.offset, buffer.data, extrasHolder.size); - } - } - return C.RESULT_BUFFER_READ; - case C.RESULT_NOTHING_READ: - return C.RESULT_NOTHING_READ; - default: - throw new IllegalStateException(); + int result = + readSampleMetadata( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilUs, extrasHolder); + if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream() && !buffer.isFlagsOnly()) { + sampleDataQueue.readToBuffer(buffer, extrasHolder); } + return result; } /** - * Reads encryption data for the current sample. - *

    - * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and - * {@link SampleExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The - * same value is added to {@link SampleExtrasHolder#offset}. + * Attempts to seek the read position to the specified sample index. * - * @param buffer The buffer into which the encryption data should be written. - * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + * @param sampleIndex The sample index. + * @return Whether the seek was successful. */ - private void readEncryptionData(DecoderInputBuffer buffer, SampleExtrasHolder extrasHolder) { - long offset = extrasHolder.offset; - - // Read the signal byte. - scratch.reset(1); - 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 (buffer.cryptoInfo.iv == null) { - buffer.cryptoInfo.iv = new byte[16]; + public final synchronized boolean seekTo(int sampleIndex) { + rewind(); + if (sampleIndex < absoluteFirstIndex || sampleIndex > absoluteFirstIndex + length) { + return false; } - readData(offset, buffer.cryptoInfo.iv, ivSize); - offset += ivSize; - - // Read the subsample count, if present. - int subsampleCount; - if (subsampleEncryption) { - scratch.reset(2); - readData(offset, scratch.data, 2); - offset += 2; - subsampleCount = scratch.readUnsignedShort(); - } else { - subsampleCount = 1; - } - - // Write the clear and encrypted subsample sizes. - int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData; - if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { - clearDataSizes = new int[subsampleCount]; - } - int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData; - if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { - encryptedDataSizes = new int[subsampleCount]; - } - if (subsampleEncryption) { - int subsampleDataLength = 6 * subsampleCount; - scratch.reset(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] = extrasHolder.size - (int) (offset - extrasHolder.offset); - } - - // Populate the cryptoInfo. - CryptoData cryptoData = extrasHolder.cryptoData; - buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, - cryptoData.encryptionKey, buffer.cryptoInfo.iv, cryptoData.cryptoMode, - cryptoData.encryptedBlocks, cryptoData.clearBlocks); - - // Adjust the offset and size to take into account the bytes read. - int bytesRead = (int) (offset - extrasHolder.offset); - extrasHolder.offset += bytesRead; - extrasHolder.size -= bytesRead; + readPosition = sampleIndex - absoluteFirstIndex; + return true; } /** - * Reads data from the front of the rolling buffer. + * Attempts to seek the read position to the keyframe before or at the specified time. * - * @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. + * @param timeUs The time to seek to. + * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the + * end of the queue, by seeking to the last sample (or keyframe). + * @return Whether the seek was successful. */ - private void readData(long absolutePosition, ByteBuffer target, int length) { - advanceReadTo(absolutePosition); - int remaining = length; - while (remaining > 0) { - int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); - Allocation allocation = readAllocationNode.allocation; - target.put(allocation.data, readAllocationNode.translateOffset(absolutePosition), toCopy); - remaining -= toCopy; - absolutePosition += toCopy; - if (absolutePosition == readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } + public final synchronized boolean seekTo(long timeUs, boolean allowTimeBeyondBuffer) { + rewind(); + int relativeReadIndex = getRelativeIndex(readPosition); + if (!hasNextSample() + || timeUs < timesUs[relativeReadIndex] + || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { + return false; } + int offset = + findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true); + if (offset == -1) { + return false; + } + readPosition += offset; + return true; } /** - * Reads data from the front of the rolling buffer. + * Advances the read position to the keyframe before or at the specified time. * - * @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. + * @param timeUs The time to advance to. + * @return The number of samples that were skipped, which may be equal to 0. */ - private void readData(long absolutePosition, byte[] target, int length) { - advanceReadTo(absolutePosition); - int remaining = length; - while (remaining > 0) { - int toCopy = Math.min(remaining, (int) (readAllocationNode.endPosition - absolutePosition)); - Allocation allocation = readAllocationNode.allocation; - System.arraycopy(allocation.data, readAllocationNode.translateOffset(absolutePosition), - target, length - remaining, toCopy); - remaining -= toCopy; - absolutePosition += toCopy; - if (absolutePosition == readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } + public final synchronized int advanceTo(long timeUs) { + int relativeReadIndex = getRelativeIndex(readPosition); + if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]) { + return 0; } + int offset = + findSampleBefore(relativeReadIndex, length - readPosition, timeUs, /* keyframe= */ true); + if (offset == -1) { + return 0; + } + readPosition += offset; + return offset; } /** - * Advances {@link #readAllocationNode} to the specified absolute position. + * Advances the read position to the end of the queue. * - * @param absolutePosition The position to which {@link #readAllocationNode} should be advanced. + * @return The number of samples that were skipped. */ - private void advanceReadTo(long absolutePosition) { - while (absolutePosition >= readAllocationNode.endPosition) { - readAllocationNode = readAllocationNode.next; - } + public final synchronized int advanceToEnd() { + int skipCount = length - readPosition; + readPosition = length; + return skipCount; } /** - * Advances {@link #firstAllocationNode} to the specified absolute position. - * {@link #readAllocationNode} is also advanced if necessary to avoid it falling behind - * {@link #firstAllocationNode}. Nodes that have been advanced past are cleared, and their - * underlying allocations are returned to the allocator. + * Discards up to but not including the sample immediately before or at the specified time. * - * @param absolutePosition The position to which {@link #firstAllocationNode} should be advanced. - * May be {@link C#POSITION_UNSET}, in which case calling this method is a no-op. + * @param timeUs The time to discard up to. + * @param toKeyframe If true then discards samples up to the keyframe before or at the specified + * time, rather than any sample before or at that time. + * @param stopAtReadPosition If true then samples are only discarded if they're before the read + * position. If false then samples at and beyond the read position may be discarded, in which + * case the read position is advanced to the first remaining sample. */ - private void discardDownstreamTo(long absolutePosition) { - if (absolutePosition == C.POSITION_UNSET) { - return; - } - while (absolutePosition >= firstAllocationNode.endPosition) { - allocator.release(firstAllocationNode.allocation); - firstAllocationNode = firstAllocationNode.clear(); - } - // If we discarded the node referenced by readAllocationNode then we need to advance it to the - // first remaining node. - if (readAllocationNode.startPosition < firstAllocationNode.startPosition) { - readAllocationNode = firstAllocationNode; - } + public final void discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + sampleDataQueue.discardDownstreamTo( + discardSampleMetadataTo(timeUs, toKeyframe, stopAtReadPosition)); + } + + /** Discards up to but not including the read position. */ + public final void discardToRead() { + sampleDataQueue.discardDownstreamTo(discardSampleMetadataToRead()); + } + + /** Discards all samples in the queue and advances the read position. */ + public final void discardToEnd() { + sampleDataQueue.discardDownstreamTo(discardSampleMetadataToEnd()); } // Called by the loading thread. + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that + * are subsequently queued. + * + * @param sampleOffsetUs The timestamp offset in microseconds. + */ + public final void setSampleOffsetUs(long sampleOffsetUs) { + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + invalidateUpstreamFormatAdjustment(); + } + } + /** * Sets a listener to be notified of changes to the upstream format. * * @param listener The listener. */ - public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { + public final void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { upstreamFormatChangeListener = listener; } - /** - * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples - * that are subsequently queued. - * - * @param sampleOffsetUs The timestamp offset in microseconds. - */ - public void setSampleOffsetUs(long sampleOffsetUs) { - if (this.sampleOffsetUs != sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - pendingFormatAdjustment = true; + // TrackOutput implementation. Called by the loading thread. + + @Override + public final void format(Format unadjustedUpstreamFormat) { + Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(unadjustedUpstreamFormat); + pendingUpstreamFormatAdjustment = false; + this.unadjustedUpstreamFormat = unadjustedUpstreamFormat; + boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat); + if (upstreamFormatChangeListener != null && upstreamFormatChanged) { + upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat); } } @Override - public void format(Format format) { - Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); - boolean formatChanged = metadataQueue.format(adjustedFormat); - lastUnadjustedFormat = format; - pendingFormatAdjustment = false; - if (upstreamFormatChangeListener != null && formatChanged) { - upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); - } - } - - @Override - public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + public final int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) throws IOException, InterruptedException { - length = preAppend(length); - int bytesAppended = input.read(writeAllocationNode.allocation.data, - writeAllocationNode.translateOffset(totalBytesWritten), length); - if (bytesAppended == C.RESULT_END_OF_INPUT) { - if (allowEndOfInput) { - return C.RESULT_END_OF_INPUT; - } - throw new EOFException(); - } - postAppend(bytesAppended); - return bytesAppended; + return sampleDataQueue.sampleData(input, length, allowEndOfInput); } @Override - public void sampleData(ParsableByteArray buffer, int length) { - while (length > 0) { - int bytesAppended = preAppend(length); - buffer.readBytes(writeAllocationNode.allocation.data, - writeAllocationNode.translateOffset(totalBytesWritten), bytesAppended); - length -= bytesAppended; - postAppend(bytesAppended); - } + public final void sampleData(ParsableByteArray buffer, int length) { + sampleDataQueue.sampleData(buffer, length); } @Override - public void sampleMetadata( + public final void sampleMetadata( long timeUs, @C.BufferFlags int flags, int size, int offset, @Nullable CryptoData cryptoData) { - if (pendingFormatAdjustment) { - format(lastUnadjustedFormat); + if (pendingUpstreamFormatAdjustment) { + format(unadjustedUpstreamFormat); } timeUs += sampleOffsetUs; if (pendingSplice) { - if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !metadataQueue.attemptSplice(timeUs)) { + if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !attemptSplice(timeUs)) { return; } pendingSplice = false; } - long absoluteOffset = totalBytesWritten - size - offset; - metadataQueue.commitSample(timeUs, flags, absoluteOffset, size, cryptoData); + long absoluteOffset = sampleDataQueue.getTotalBytesWritten() - size - offset; + commitSample(timeUs, flags, absoluteOffset, size, cryptoData); } - // Private methods. - /** - * Clears allocation nodes starting from {@code fromNode}. - * - * @param fromNode The node from which to clear. + * Invalidates the last upstream format adjustment. {@link #getAdjustedUpstreamFormat(Format)} + * will be called to adjust the upstream {@link Format} again before the next sample is queued. */ - private void clearAllocationNodes(AllocationNode fromNode) { - if (!fromNode.wasInitialized) { - return; - } - // Bulk release allocations for performance (it's significantly faster when using - // DefaultAllocator because the allocator's lock only needs to be acquired and released once) - // [Internal: See b/29542039]. - int allocationCount = (writeAllocationNode.wasInitialized ? 1 : 0) - + ((int) (writeAllocationNode.startPosition - fromNode.startPosition) / allocationLength); - Allocation[] allocationsToRelease = new Allocation[allocationCount]; - AllocationNode currentNode = fromNode; - for (int i = 0; i < allocationsToRelease.length; i++) { - allocationsToRelease[i] = currentNode.allocation; - currentNode = currentNode.clear(); - } - allocator.release(allocationsToRelease); + protected final void invalidateUpstreamFormatAdjustment() { + pendingUpstreamFormatAdjustment = true; } /** - * Called before writing sample data to {@link #writeAllocationNode}. May cause - * {@link #writeAllocationNode} to be initialized. + * Adjusts the upstream {@link Format} (i.e., the {@link Format} that was most recently passed to + * {@link #format(Format)}). * - * @param length The number of bytes that the caller wishes to write. - * @return The number of bytes that the caller is permitted to write, which may be less than - * {@code length}. - */ - private int preAppend(int length) { - if (!writeAllocationNode.wasInitialized) { - writeAllocationNode.initialize(allocator.allocate(), - new AllocationNode(writeAllocationNode.endPosition, allocationLength)); - } - return Math.min(length, (int) (writeAllocationNode.endPosition - totalBytesWritten)); - } - - /** - * Called after writing sample data. May cause {@link #writeAllocationNode} to be advanced. - * - * @param length The number of bytes that were written. - */ - private void postAppend(int length) { - totalBytesWritten += length; - if (totalBytesWritten == writeAllocationNode.endPosition) { - writeAllocationNode = writeAllocationNode.next; - } - } - - /** - * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}. + *

    The default implementation incorporates the sample offset passed to {@link + * #setSampleOffsetUs(long)} into {@link Format#subsampleOffsetUs}. * * @param format The {@link Format} to adjust. - * @param sampleOffsetUs The offset to apply. * @return The adjusted {@link Format}. */ - private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) { - if (format == null) { - return null; - } + @CallSuper + protected Format getAdjustedUpstreamFormat(Format format) { if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs); } return format; } - /** - * A node in a linked list of {@link Allocation}s held by the output. - */ - private static final class AllocationNode { - - /** - * The absolute position of the start of the data (inclusive). - */ - public final long startPosition; - /** - * The absolute position of the end of the data (exclusive). - */ - public final long endPosition; - /** - * Whether the node has been initialized. Remains true after {@link #clear()}. - */ - public boolean wasInitialized; - /** - * The {@link Allocation}, or {@code null} if the node is not initialized. - */ - @Nullable public Allocation allocation; - /** - * The next {@link AllocationNode} in the list, or {@code null} if the node has not been - * initialized. Remains set after {@link #clear()}. - */ - @Nullable public AllocationNode next; - - /** - * @param startPosition See {@link #startPosition}. - * @param allocationLength The length of the {@link Allocation} with which this node will be - * initialized. - */ - public AllocationNode(long startPosition, int allocationLength) { - this.startPosition = startPosition; - this.endPosition = startPosition + allocationLength; - } - - /** - * Initializes the node. - * - * @param allocation The node's {@link Allocation}. - * @param next The next {@link AllocationNode}. - */ - public void initialize(Allocation allocation, AllocationNode next) { - this.allocation = allocation; - this.next = next; - wasInitialized = true; - } - - /** - * Gets the offset into the {@link #allocation}'s {@link Allocation#data} that corresponds to - * the specified absolute position. - * - * @param absolutePosition The absolute position. - * @return The corresponding offset into the allocation's data. - */ - public int translateOffset(long absolutePosition) { - return (int) (absolutePosition - startPosition) + allocation.offset; - } - - /** - * Clears {@link #allocation} and {@link #next}. - * - * @return The cleared next {@link AllocationNode}. - */ - public AllocationNode clear() { - allocation = null; - AllocationNode temp = next; - next = null; - return temp; - } + // Internal methods. + /** Rewinds the read position to the first sample in the queue. */ + private synchronized void rewind() { + readPosition = 0; + sampleDataQueue.rewind(); } + @SuppressWarnings("ReferenceEquality") // See comments in setUpstreamFormat + private synchronized int readSampleMetadata( + FormatHolder formatHolder, + DecoderInputBuffer buffer, + boolean formatRequired, + boolean loadingFinished, + long decodeOnlyUntilUs, + SampleExtrasHolder extrasHolder) { + buffer.waitingForKeys = false; + // This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155. + // TODO: Remove it and replace it with a fix that discards samples when writing to the queue. + boolean hasNextSample; + int relativeReadIndex = C.INDEX_UNSET; + while ((hasNextSample = hasNextSample())) { + relativeReadIndex = getRelativeIndex(readPosition); + long timeUs = timesUs[relativeReadIndex]; + if (timeUs < decodeOnlyUntilUs + && MimeTypes.allSamplesAreSyncSamples(formats[relativeReadIndex].sampleMimeType)) { + readPosition++; + } else { + break; + } + } + + if (!hasNextSample) { + if (loadingFinished || isLastSampleQueued) { + buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (upstreamFormat != null && (formatRequired || upstreamFormat != downstreamFormat)) { + onFormatResult(Assertions.checkNotNull(upstreamFormat), formatHolder); + return C.RESULT_FORMAT_READ; + } else { + return C.RESULT_NOTHING_READ; + } + } + + if (formatRequired || formats[relativeReadIndex] != downstreamFormat) { + onFormatResult(formats[relativeReadIndex], formatHolder); + return C.RESULT_FORMAT_READ; + } + + if (!mayReadSample(relativeReadIndex)) { + buffer.waitingForKeys = true; + return C.RESULT_NOTHING_READ; + } + + buffer.setFlags(flags[relativeReadIndex]); + buffer.timeUs = timesUs[relativeReadIndex]; + if (buffer.timeUs < decodeOnlyUntilUs) { + buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } + if (buffer.isFlagsOnly()) { + return C.RESULT_BUFFER_READ; + } + extrasHolder.size = sizes[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.cryptoData = cryptoDatas[relativeReadIndex]; + + readPosition++; + return C.RESULT_BUFFER_READ; + } + + private synchronized boolean setUpstreamFormat(Format format) { + if (format == null) { + upstreamFormatRequired = true; + return false; + } + upstreamFormatRequired = false; + if (Util.areEqual(format, upstreamFormat)) { + // The format is unchanged. If format and upstreamFormat are different objects, we keep the + // current upstreamFormat so we can detect format changes on the read side using cheap + // referential quality. + return false; + } else if (Util.areEqual(format, upstreamCommittedFormat)) { + // The format has changed back to the format of the last committed sample. If they are + // different objects, we revert back to using upstreamCommittedFormat as the upstreamFormat + // so we can detect format changes on the read side using cheap referential equality. + upstreamFormat = upstreamCommittedFormat; + return true; + } else { + upstreamFormat = format; + return true; + } + } + + private synchronized long discardSampleMetadataTo( + long timeUs, boolean toKeyframe, boolean stopAtReadPosition) { + if (length == 0 || timeUs < timesUs[relativeFirstIndex]) { + return C.POSITION_UNSET; + } + int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length; + int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe); + if (discardCount == -1) { + return C.POSITION_UNSET; + } + return discardSamples(discardCount); + } + + public synchronized long discardSampleMetadataToRead() { + if (readPosition == 0) { + return C.POSITION_UNSET; + } + return discardSamples(readPosition); + } + + private synchronized long discardSampleMetadataToEnd() { + if (length == 0) { + return C.POSITION_UNSET; + } + return discardSamples(length); + } + + private void releaseDrmSessionReferences() { + if (currentDrmSession != null) { + currentDrmSession.release(); + currentDrmSession = null; + // Clear downstream format to avoid violating the assumption that downstreamFormat.drmInitData + // != null implies currentSession != null + downstreamFormat = null; + } + } + + private synchronized void commitSample( + long timeUs, @C.BufferFlags int sampleFlags, long offset, int size, CryptoData cryptoData) { + if (upstreamKeyframeRequired) { + if ((sampleFlags & C.BUFFER_FLAG_KEY_FRAME) == 0) { + return; + } + upstreamKeyframeRequired = false; + } + Assertions.checkState(!upstreamFormatRequired); + + isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); + + int relativeEndIndex = getRelativeIndex(length); + timesUs[relativeEndIndex] = timeUs; + offsets[relativeEndIndex] = offset; + sizes[relativeEndIndex] = size; + flags[relativeEndIndex] = sampleFlags; + cryptoDatas[relativeEndIndex] = cryptoData; + formats[relativeEndIndex] = upstreamFormat; + sourceIds[relativeEndIndex] = upstreamSourceId; + upstreamCommittedFormat = upstreamFormat; + + length++; + if (length == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + int[] newSourceIds = new int[newCapacity]; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + CryptoData[] newCryptoDatas = new CryptoData[newCapacity]; + Format[] newFormats = new Format[newCapacity]; + int beforeWrap = capacity - relativeFirstIndex; + System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap); + System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap); + System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap); + System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap); + int afterWrap = relativeFirstIndex; + 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(cryptoDatas, 0, newCryptoDatas, beforeWrap, afterWrap); + System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap); + System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + cryptoDatas = newCryptoDatas; + formats = newFormats; + sourceIds = newSourceIds; + relativeFirstIndex = 0; + capacity = newCapacity; + } + } + + /** + * Attempts to discard samples from the end of the queue to allow samples starting from the + * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. + * + * @param timeUs The timestamp at which the splice occurs. + * @return Whether the splice was successful. + */ + private synchronized boolean attemptSplice(long timeUs) { + if (length == 0) { + return timeUs > largestDiscardedTimestampUs; + } + long largestReadTimestampUs = + Math.max(largestDiscardedTimestampUs, getLargestTimestamp(readPosition)); + if (largestReadTimestampUs >= timeUs) { + return false; + } + int retainCount = length; + int relativeSampleIndex = getRelativeIndex(length - 1); + while (retainCount > readPosition && timesUs[relativeSampleIndex] >= timeUs) { + retainCount--; + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + discardUpstreamSampleMetadata(absoluteFirstIndex + retainCount); + return true; + } + + private long discardUpstreamSampleMetadata(int discardFromIndex) { + int discardCount = getWriteIndex() - discardFromIndex; + Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); + length -= discardCount; + largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); + isLastSampleQueued = discardCount == 0 && isLastSampleQueued; + if (length != 0) { + int relativeLastWriteIndex = getRelativeIndex(length - 1); + return offsets[relativeLastWriteIndex] + sizes[relativeLastWriteIndex]; + } + return 0; + } + + private boolean hasNextSample() { + return readPosition != length; + } + + /** + * Sets the downstream format, performs DRM resource management, and populates the {@code + * outputFormatHolder}. + * + * @param newFormat The new downstream format. + * @param outputFormatHolder The output {@link FormatHolder}. + */ + private void onFormatResult(Format newFormat, FormatHolder outputFormatHolder) { + outputFormatHolder.format = newFormat; + boolean isFirstFormat = downstreamFormat == null; + DrmInitData oldDrmInitData = isFirstFormat ? null : downstreamFormat.drmInitData; + downstreamFormat = newFormat; + if (drmSessionManager == DrmSessionManager.DUMMY) { + // Avoid attempting to acquire a session using the dummy DRM session manager. It's likely that + // the media source creation has not yet been migrated and the renderer can acquire the + // session for the read DRM init data. + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + return; + } + DrmInitData newDrmInitData = newFormat.drmInitData; + outputFormatHolder.includesDrmSession = true; + outputFormatHolder.drmSession = currentDrmSession; + if (!isFirstFormat && Util.areEqual(oldDrmInitData, newDrmInitData)) { + // Nothing to do. + return; + } + // Ensure we acquire the new session before releasing the previous one in case the same session + // is being used for both DrmInitData. + @Nullable DrmSession previousSession = currentDrmSession; + currentDrmSession = + newDrmInitData != null + ? drmSessionManager.acquireSession(playbackLooper, newDrmInitData) + : drmSessionManager.acquirePlaceholderSession( + playbackLooper, MimeTypes.getTrackType(newFormat.sampleMimeType)); + outputFormatHolder.drmSession = currentDrmSession; + + if (previousSession != null) { + previousSession.release(); + } + } + + /** + * Returns whether it's possible to read the next sample. + * + * @param relativeReadIndex The relative read index of the next sample. + * @return Whether it's possible to read the next sample. + */ + private boolean mayReadSample(int relativeReadIndex) { + if (drmSessionManager == DrmSessionManager.DUMMY) { + // TODO: Remove once renderers are migrated [Internal ref: b/122519809]. + // For protected content it's likely that the DrmSessionManager is still being injected into + // the renderers. We assume that the renderers will be able to acquire a DrmSession if needed. + return true; + } + return currentDrmSession == null + || currentDrmSession.getState() == DrmSession.STATE_OPENED_WITH_KEYS + || ((flags[relativeReadIndex] & C.BUFFER_FLAG_ENCRYPTED) == 0 + && currentDrmSession.playClearSamplesWithoutKeys()); + } + + /** + * Finds the sample in the specified range that's before or at the specified time. If {@code + * keyframe} is {@code true} then the sample is additionally required to be a keyframe. + * + * @param relativeStartIndex The relative index from which to start searching. + * @param length The length of the range being searched. + * @param timeUs The specified time. + * @param keyframe Whether only keyframes should be considered. + * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching + * sample was found. + */ + private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) { + // This could be optimized to use a binary search, however in practice callers to this method + // normally pass times near to the start of the search region. Hence it's unclear whether + // switching to a binary search would yield any real benefit. + int sampleCountToTarget = -1; + int searchIndex = relativeStartIndex; + for (int i = 0; i < length && timesUs[searchIndex] <= timeUs; i++) { + if (!keyframe || (flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + // We've found a suitable sample. + sampleCountToTarget = i; + } + searchIndex++; + if (searchIndex == capacity) { + searchIndex = 0; + } + } + return sampleCountToTarget; + } + + /** + * Discards the specified number of samples. + * + * @param discardCount The number of samples to discard. + * @return The corresponding offset up to which data should be discarded. + */ + private long discardSamples(int discardCount) { + largestDiscardedTimestampUs = + Math.max(largestDiscardedTimestampUs, getLargestTimestamp(discardCount)); + length -= discardCount; + absoluteFirstIndex += discardCount; + relativeFirstIndex += discardCount; + if (relativeFirstIndex >= capacity) { + relativeFirstIndex -= capacity; + } + readPosition -= discardCount; + if (readPosition < 0) { + readPosition = 0; + } + if (length == 0) { + int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1; + return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex]; + } else { + return offsets[relativeFirstIndex]; + } + } + + /** + * Finds the largest timestamp of any sample from the start of the queue up to the specified + * length, assuming that the timestamps prior to a keyframe are always less than the timestamp of + * the keyframe itself, and of subsequent frames. + * + * @param length The length of the range being searched. + * @return The largest timestamp, or {@link Long#MIN_VALUE} if {@code length == 0}. + */ + private long getLargestTimestamp(int length) { + if (length == 0) { + return Long.MIN_VALUE; + } + long largestTimestampUs = Long.MIN_VALUE; + int relativeSampleIndex = getRelativeIndex(length - 1); + for (int i = 0; i < length; i++) { + largestTimestampUs = Math.max(largestTimestampUs, timesUs[relativeSampleIndex]); + if ((flags[relativeSampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) { + break; + } + relativeSampleIndex--; + if (relativeSampleIndex == -1) { + relativeSampleIndex = capacity - 1; + } + } + return largestTimestampUs; + } + + /** + * Returns the relative index for a given offset from the start of the queue. + * + * @param offset The offset, which must be in the range [0, length]. + */ + private int getRelativeIndex(int offset) { + int relativeIndex = relativeFirstIndex + offset; + return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity; + } + + /** A holder for sample metadata not held by {@link DecoderInputBuffer}. */ + /* package */ static final class SampleExtrasHolder { + + public int size; + public long offset; + public CryptoData cryptoData; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index 182f0f17c..189c13ef0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -60,6 +60,9 @@ public interface SequenceableLoader { */ boolean continueLoading(long positionUs); + /** Returns whether the loader is currently loading. */ + boolean isLoading(); + /** * Re-evaluates the buffer given the playback position. * diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java index 72095c2c5..773eba732 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SilenceMediaSource.java @@ -33,6 +33,42 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Media source with a single period consisting of silent raw audio of a given duration. */ public final class SilenceMediaSource extends BaseMediaSource { + /** Factory for {@link SilenceMediaSource SilenceMediaSources}. */ + public static final class Factory { + + private long durationUs; + @Nullable private Object tag; + + /** + * Sets the duration of the silent audio. + * + * @param durationUs The duration of silent audio to output, in microseconds. + * @return This factory, for convenience. + */ + public Factory setDurationUs(long durationUs) { + this.durationUs = durationUs; + return this; + } + + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + */ + public Factory setTag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + /** Creates a new {@link SilenceMediaSource}. */ + public SilenceMediaSource createMediaSource() { + return new SilenceMediaSource(durationUs, tag); + } + } + private static final int SAMPLE_RATE_HZ = 44100; @C.PcmEncoding private static final int ENCODING = C.ENCODING_PCM_16BIT; private static final int CHANNEL_COUNT = 2; @@ -54,6 +90,7 @@ public final class SilenceMediaSource extends BaseMediaSource { new byte[Util.getPcmFrameSize(ENCODING, CHANNEL_COUNT) * 1024]; private final long durationUs; + @Nullable private final Object tag; /** * Creates a new media source providing silent audio of the given duration. @@ -61,15 +98,25 @@ public final class SilenceMediaSource extends BaseMediaSource { * @param durationUs The duration of silent audio to output, in microseconds. */ public SilenceMediaSource(long durationUs) { + this(durationUs, /* tag= */ null); + } + + private SilenceMediaSource(long durationUs, @Nullable Object tag) { Assertions.checkArgument(durationUs >= 0); this.durationUs = durationUs; + this.tag = tag; } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { refreshSourceInfo( - new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false), - /* manifest= */ null); + new SinglePeriodTimeline( + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* manifest= */ null, + tag)); } @Override @@ -84,7 +131,7 @@ public final class SilenceMediaSource extends BaseMediaSource { public void releasePeriod(MediaPeriod mediaPeriod) {} @Override - public void releaseSourceInternal() {} + protected void releaseSourceInternal() {} private static final class SilenceMediaPeriod implements MediaPeriod { @@ -172,6 +219,11 @@ public final class SilenceMediaSource extends BaseMediaSource { return false; } + @Override + public boolean isLoading() { + return false; + } + @Override public void reevaluateBuffer(long positionUs) {} @@ -221,9 +273,9 @@ public final class SilenceMediaSource extends BaseMediaSource { int bytesToWrite = (int) Math.min(SILENCE_SAMPLE.length, bytesRemaining); buffer.ensureSpaceForWrite(bytesToWrite); - buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); buffer.data.put(SILENCE_SAMPLE, /* offset= */ 0, bytesToWrite); buffer.timeUs = getAudioPositionUs(positionBytes); + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); positionBytes += bytesToWrite; return C.RESULT_BUFFER_READ; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index acdfbcc8c..45f64cacf 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -35,7 +35,9 @@ public final class SinglePeriodTimeline extends Timeline { private final long windowDefaultStartPositionUs; private final boolean isSeekable; private final boolean isDynamic; - private final @Nullable Object tag; + private final boolean isLive; + @Nullable private final Object tag; + @Nullable private final Object manifest; /** * Creates a timeline containing a single period and a window that spans it. @@ -43,9 +45,11 @@ public final class SinglePeriodTimeline extends Timeline { * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { - this(durationUs, isSeekable, isDynamic, /* tag= */ null); + public SinglePeriodTimeline( + long durationUs, boolean isSeekable, boolean isDynamic, boolean isLive) { + this(durationUs, isSeekable, isDynamic, isLive, /* manifest= */ null, /* tag= */ null); } /** @@ -54,10 +58,17 @@ public final class SinglePeriodTimeline extends Timeline { * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. * @param isDynamic Whether the window may change when the timeline is updated. - * @param tag A tag used for {@link Timeline.Window#tag}. + * @param isLive Whether the window is live. + * @param manifest The manifest. May be {@code null}. + * @param tag A tag used for {@link Window#tag}. */ public SinglePeriodTimeline( - long durationUs, boolean isSeekable, boolean isDynamic, @Nullable Object tag) { + long durationUs, + boolean isSeekable, + boolean isDynamic, + boolean isLive, + @Nullable Object manifest, + @Nullable Object tag) { this( durationUs, durationUs, @@ -65,6 +76,8 @@ public final class SinglePeriodTimeline extends Timeline { /* windowDefaultStartPositionUs= */ 0, isSeekable, isDynamic, + isLive, + manifest, tag); } @@ -80,6 +93,8 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. + * @param manifest The manifest. May be (@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ public SinglePeriodTimeline( @@ -89,6 +104,8 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + boolean isLive, + @Nullable Object manifest, @Nullable Object tag) { this( /* presentationStartTimeMs= */ C.TIME_UNSET, @@ -99,6 +116,8 @@ public final class SinglePeriodTimeline extends Timeline { windowDefaultStartPositionUs, isSeekable, isDynamic, + isLive, + manifest, tag); } @@ -117,6 +136,8 @@ public final class SinglePeriodTimeline extends Timeline { * which to begin playback, in microseconds. * @param isSeekable Whether seeking is supported within the window. * @param isDynamic Whether the window may change when the timeline is updated. + * @param isLive Whether the window is live. + * @param manifest The manifest. May be {@code null}. * @param tag A tag used for {@link Timeline.Window#tag}. */ public SinglePeriodTimeline( @@ -128,6 +149,8 @@ public final class SinglePeriodTimeline extends Timeline { long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic, + boolean isLive, + @Nullable Object manifest, @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; @@ -137,6 +160,8 @@ public final class SinglePeriodTimeline extends Timeline { this.windowDefaultStartPositionUs = windowDefaultStartPositionUs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; + this.isLive = isLive; + this.manifest = manifest; this.tag = tag; } @@ -146,10 +171,8 @@ public final class SinglePeriodTimeline extends Timeline { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); - Object tag = setTag ? this.tag : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; if (isDynamic && defaultPositionProjectionUs != 0) { if (windowDurationUs == C.TIME_UNSET) { @@ -164,11 +187,14 @@ public final class SinglePeriodTimeline extends Timeline { } } return window.set( + Window.SINGLE_WINDOW_UID, tag, + manifest, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, + isLive, windowDefaultStartPositionUs, windowDurationUs, 0, diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 44fca3cb9..ca50c342b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -31,11 +31,14 @@ import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.upstream.StatsDataSource; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaPeriod} with a single sample. @@ -50,7 +53,7 @@ import java.util.Arrays; private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; @@ -64,8 +67,7 @@ import java.util.Arrays; /* package */ boolean notifiedReadingStarted; /* package */ boolean loadingFinished; - /* package */ boolean loadingSucceeded; - /* package */ byte[] sampleData; + /* package */ byte @MonotonicNonNull [] sampleData; /* package */ int sampleSize; public SingleSampleMediaPeriod( @@ -112,8 +114,12 @@ import java.util.Arrays; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { sampleStreams.remove(streams[i]); @@ -166,6 +172,11 @@ import java.util.Arrays; return true; } + @Override + public boolean isLoading() { + return loader.isLoading(); + } + @Override public long readDiscontinuity() { if (!notifiedReadingStarted) { @@ -204,9 +215,8 @@ import java.util.Arrays; public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { sampleSize = (int) loadable.dataSource.getBytesRead(); - sampleData = loadable.sampleData; + sampleData = Assertions.checkNotNull(loadable.sampleData); loadingFinished = true; - loadingSucceeded = true; eventDispatcher.loadCompleted( loadable.dataSpec, loadable.dataSource.getLastOpenedUri(), @@ -325,7 +335,7 @@ import java.util.Arrays; streamState = STREAM_STATE_SEND_SAMPLE; return C.RESULT_FORMAT_READ; } else if (loadingFinished) { - if (loadingSucceeded) { + if (sampleData != null) { buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); buffer.timeUs = 0; if (buffer.isFlagsOnly()) { @@ -371,8 +381,10 @@ import java.util.Arrays; private final StatsDataSource dataSource; - private byte[] sampleData; + @Nullable private byte[] sampleData; + // the constructor does not initialize fields: sampleData + @SuppressWarnings("nullness:initialization.fields.uninitialized") public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { this.dataSpec = dataSpec; this.dataSource = new StatsDataSource(dataSource); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 6f85a2b0f..db1414942 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -60,7 +60,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean treatLoadErrorsAsEndOfStream; private boolean isCreateCalled; - private @Nullable Object tag; + @Nullable private Object tag; /** * Creates a factory for {@link SingleSampleMediaSource}s. @@ -186,7 +186,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private final Timeline timeline; @Nullable private final Object tag; - private @Nullable TransferListener transferListener; + @Nullable private TransferListener transferListener; /** * @param uri The {@link Uri} of the media stream. @@ -290,7 +290,13 @@ public final class SingleSampleMediaSource extends BaseMediaSource { this.tag = tag; dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); timeline = - new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false, tag); + new SinglePeriodTimeline( + durationUs, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* isLive= */ false, + /* manifest= */ null, + tag); } // MediaSource implementation. @@ -302,9 +308,9 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; - refreshSourceInfo(timeline, /* manifest= */ null); + refreshSourceInfo(timeline); } @Override @@ -331,7 +337,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { // Do nothing. } @@ -341,7 +347,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { */ @Deprecated @SuppressWarnings("deprecation") - private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { + private static final class EventListenerWrapper implements MediaSourceEventListener { private final EventListener eventListener; private final int eventSourceId; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index be9dea91f..3a093ca79 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -18,16 +18,18 @@ package com.google.android.exoplayer2.source.ads; import android.net.Uri; import androidx.annotation.CheckResult; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** - * Represents ad group times relative to the start of the media and information on the state and - * URIs of ads within each ad group. + * Represents ad group times and information on the state and URIs of ads within each ad group. * *

    Instances are immutable. Call the {@code with*} methods to get new instances that have the * required changes. @@ -45,9 +47,9 @@ public final class AdPlaybackState { /** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */ public final int count; /** The URI of each ad in the ad group. */ - public final Uri[] uris; + public final @NullableType Uri[] uris; /** The state of each ad in the ad group. */ - public final @AdState int[] states; + @AdState public final int[] states; /** The durations of each ad in the ad group, in microseconds. */ public final long[] durationsUs; @@ -60,7 +62,8 @@ public final class AdPlaybackState { /* durationsUs= */ new long[0]); } - private AdGroup(int count, @AdState int[] states, Uri[] uris, long[] durationsUs) { + private AdGroup( + int count, @AdState int[] states, @NullableType Uri[] uris, long[] durationsUs) { Assertions.checkArgument(states.length == uris.length); this.count = count; this.states = states; @@ -98,7 +101,7 @@ public final class AdPlaybackState { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -121,37 +124,27 @@ public final class AdPlaybackState { return result; } - /** - * Returns a new instance with the ad count set to {@code count}. This method may only be called - * if this instance's ad count has not yet been specified. - */ + /** Returns a new instance with the ad count set to {@code count}. */ @CheckResult public AdGroup withAdCount(int count) { - Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count); @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count); long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count); - Uri[] uris = Arrays.copyOf(this.uris, count); + @NullableType Uri[] uris = Arrays.copyOf(this.uris, count); return new AdGroup(count, states, uris, durationsUs); } /** * Returns a new instance with the specified {@code uri} set for the specified ad, and the ad - * marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link - * #AD_STATE_UNAVAILABLE}, which is the default state. - * - *

    This instance's ad count may be unknown, in which case {@code index} must be less than the - * ad count specified later. Otherwise, {@code index} must be less than the current ad count. + * marked as {@link #AD_STATE_AVAILABLE}. */ @CheckResult public AdGroup withAdUri(Uri uri, int index) { - Assertions.checkArgument(count == C.LENGTH_UNSET || index < count); @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1); - Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE); long[] durationsUs = this.durationsUs.length == states.length ? this.durationsUs : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); - Uri[] uris = Arrays.copyOf(this.uris, states.length); + @NullableType Uri[] uris = Arrays.copyOf(this.uris, states.length); uris[index] = uri; states[index] = AD_STATE_AVAILABLE; return new AdGroup(count, states, uris, durationsUs); @@ -177,6 +170,7 @@ public final class AdPlaybackState { this.durationsUs.length == states.length ? this.durationsUs : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length); + @NullableType Uri[] uris = this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length); states[index] = state; @@ -267,22 +261,26 @@ public final class AdPlaybackState { /** The number of ad groups. */ public final int adGroupCount; /** - * The times of ad groups, in microseconds. A final element with the value {@link - * C#TIME_END_OF_SOURCE} indicates a postroll ad. + * The times of ad groups, in microseconds, relative to the start of the {@link + * com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with the value + * {@link C#TIME_END_OF_SOURCE} indicates a postroll ad. */ public final long[] adGroupTimesUs; /** The ad groups. */ public final AdGroup[] adGroups; /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */ public final long adResumePositionUs; - /** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */ + /** + * The duration of the content period in microseconds, if known. {@link C#TIME_UNSET} otherwise. + */ public final long contentDurationUs; /** * Creates a new ad playback state with the specified ad group times. * - * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value - * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. + * @param adGroupTimesUs The times of ad groups in microseconds, relative to the start of the + * {@link com.google.android.exoplayer2.Timeline.Period} they belong to. A final element with + * the value {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. */ public AdPlaybackState(long... adGroupTimesUs) { int count = adGroupTimesUs.length; @@ -310,16 +308,18 @@ public final class AdPlaybackState { * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no * ads remaining to be played, or if there is no such ad group. * - * @param positionUs The position at or before which to find an ad group, in microseconds, or - * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any + * @param positionUs The period position at or before which to find an ad group, in microseconds, + * or {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case the index of any * unplayed postroll ad group will be returned). + * @param periodDurationUs The duration of the containing timeline period, in microseconds, or + * {@link C#TIME_UNSET} if not known. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ - public int getAdGroupIndexForPositionUs(long positionUs) { + public int getAdGroupIndexForPositionUs(long positionUs, long periodDurationUs) { // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. // In practice we expect there to be few ad groups so the search shouldn't be expensive. int index = adGroupTimesUs.length - 1; - while (index >= 0 && isPositionBeforeAdGroup(positionUs, index)) { + while (index >= 0 && isPositionBeforeAdGroup(positionUs, periodDurationUs, index)) { index--; } return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET; @@ -329,11 +329,11 @@ public final class AdPlaybackState { * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * - * @param positionUs The position after which to find an ad group, in microseconds, or {@link - * C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad group - * after the position). - * @param periodDurationUs The duration of the containing period in microseconds, or {@link - * C#TIME_UNSET} if not known. + * @param positionUs The period position after which to find an ad group, in microseconds, or + * {@link C#TIME_END_OF_SOURCE} for the end of the stream (in which case there can be no ad + * group after the position). + * @param periodDurationUs The duration of the containing timeline period, in microseconds, or + * {@link C#TIME_UNSET} if not known. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs, long periodDurationUs) { @@ -352,6 +352,18 @@ public final class AdPlaybackState { return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; } + /** Returns whether the specified ad has been marked as in {@link #AD_STATE_ERROR}. */ + public boolean isAdInErrorState(int adGroupIndex, int adIndexInAdGroup) { + if (adGroupIndex >= adGroups.length) { + return false; + } + AdGroup adGroup = adGroups[adGroupIndex]; + if (adGroup.count == C.LENGTH_UNSET || adIndexInAdGroup >= adGroup.count) { + return false; + } + return adGroup.states[adIndexInAdGroup] == AdPlaybackState.AD_STATE_ERROR; + } + /** * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}. * The ad count must be greater than zero. @@ -362,7 +374,7 @@ public final class AdPlaybackState { if (adGroups[adGroupIndex].count == adCount) { return this; } - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -370,7 +382,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad URI. */ @CheckResult public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -378,7 +390,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad marked as played. */ @CheckResult public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -386,7 +398,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad marked as skipped. */ @CheckResult public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -394,7 +406,7 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad marked as having a load error. */ @CheckResult public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -405,7 +417,7 @@ public final class AdPlaybackState { */ @CheckResult public AdPlaybackState withSkippedAdGroup(int adGroupIndex) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped(); return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } @@ -413,14 +425,17 @@ public final class AdPlaybackState { /** Returns an instance with the specified ad durations, in microseconds. */ @CheckResult public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) { - AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length); for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) { adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]); } return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } - /** Returns an instance with the specified ad resume position, in microseconds. */ + /** + * Returns an instance with the specified ad resume position, in microseconds, relative to the + * start of the current ad. + */ @CheckResult public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) { if (this.adResumePositionUs == adResumePositionUs) { @@ -441,7 +456,7 @@ public final class AdPlaybackState { } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (this == o) { return true; } @@ -466,14 +481,63 @@ public final class AdPlaybackState { return result; } - private boolean isPositionBeforeAdGroup(long positionUs, int adGroupIndex) { + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("AdPlaybackState(adResumePositionUs="); + sb.append(adResumePositionUs); + sb.append(", adGroups=["); + for (int i = 0; i < adGroups.length; i++) { + sb.append("adGroup(timeUs="); + sb.append(adGroupTimesUs[i]); + sb.append(", ads=["); + for (int j = 0; j < adGroups[i].states.length; j++) { + sb.append("ad(state="); + switch (adGroups[i].states[j]) { + case AD_STATE_UNAVAILABLE: + sb.append('_'); + break; + case AD_STATE_ERROR: + sb.append('!'); + break; + case AD_STATE_AVAILABLE: + sb.append('R'); + break; + case AD_STATE_PLAYED: + sb.append('P'); + break; + case AD_STATE_SKIPPED: + sb.append('S'); + break; + default: + sb.append('?'); + break; + } + sb.append(", durationUs="); + sb.append(adGroups[i].durationsUs[j]); + sb.append(')'); + if (j < adGroups[i].states.length - 1) { + sb.append(", "); + } + } + sb.append("])"); + if (i < adGroups.length - 1) { + sb.append(", "); + } + } + sb.append("])"); + return sb.toString(); + } + + private boolean isPositionBeforeAdGroup( + long positionUs, long periodDurationUs, int adGroupIndex) { if (positionUs == C.TIME_END_OF_SOURCE) { // The end of the content is at (but not before) any postroll ad, and after any other ads. return false; } long adGroupPositionUs = adGroupTimesUs[adGroupIndex]; if (adGroupPositionUs == C.TIME_END_OF_SOURCE) { - return contentDurationUs == C.TIME_UNSET || positionUs < contentDurationUs; + return periodDurationUs == C.TIME_UNSET || positionUs < periodDurationUs; } else { return positionUs < adGroupPositionUs; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 2b90fac6a..11947218a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.source.ads; -import androidx.annotation.Nullable; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.ads.AdsMediaSource.AdLoadException; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 199897796..3481042c9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -18,18 +18,20 @@ package com.google.android.exoplayer2.source.ads; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.os.SystemClock; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.CompositeMediaSource; -import com.google.android.exoplayer2.source.DeferredMediaPeriod; +import com.google.android.exoplayer2.source.MaskingMediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; @@ -43,9 +45,9 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaSource} that inserts ads linearly with a provided content media source. This source @@ -54,27 +56,6 @@ import java.util.Map; */ public final class AdsMediaSource extends CompositeMediaSource { - /** Factory for creating {@link MediaSource}s to play ad media. */ - public interface MediaSourceFactory { - - /** - * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. - * - * @param uri The URI of the media or manifest to play. - * @return The new media source. - */ - MediaSource createMediaSource(Uri uri); - - /** - * Returns the content types supported by media sources created by this factory. Each element - * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link - * C#TYPE_OTHER}. - * - * @return The content types supported by media sources created by this factory. - */ - int[] getSupportedTypes(); - } - /** * Wrapper for exceptions that occur while loading ads, which are notified via {@link * MediaSourceEventListener#onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, @@ -134,7 +115,7 @@ public final class AdsMediaSource extends CompositeMediaSource { */ public RuntimeException getRuntimeExceptionForUnexpected() { Assertions.checkState(type == TYPE_UNEXPECTED); - return (RuntimeException) getCause(); + return (RuntimeException) Assertions.checkNotNull(getCause()); } } @@ -147,16 +128,13 @@ public final class AdsMediaSource extends CompositeMediaSource { private final AdsLoader adsLoader; private final AdsLoader.AdViewProvider adViewProvider; private final Handler mainHandler; - private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; // Accessed on the player thread. - private ComponentListener componentListener; - private Timeline contentTimeline; - private Object contentManifest; - private AdPlaybackState adPlaybackState; - private MediaSource[][] adGroupMediaSources; - private Timeline[][] adGroupTimelines; + @Nullable private ComponentListener componentListener; + @Nullable private Timeline contentTimeline; + @Nullable private AdPlaybackState adPlaybackState; + private @NullableType AdMediaSourceHolder[][] adMediaSourceHolders; /** * Constructs a new source that inserts ads linearly with the content specified by {@code @@ -198,10 +176,8 @@ public final class AdsMediaSource extends CompositeMediaSource { this.adsLoader = adsLoader; this.adViewProvider = adViewProvider; mainHandler = new Handler(Looper.getMainLooper()); - deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); - adGroupMediaSources = new MediaSource[0][]; - adGroupTimelines = new Timeline[0][]; + adMediaSourceHolders = new AdMediaSourceHolder[0][]; adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } @@ -212,7 +188,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { super.prepareSourceInternal(mediaTransferListener); ComponentListener componentListener = new ComponentListener(); this.componentListener = componentListener; @@ -222,43 +198,30 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + AdPlaybackState adPlaybackState = Assertions.checkNotNull(this.adPlaybackState); if (adPlaybackState.adGroupCount > 0 && id.isAd()) { int adGroupIndex = id.adGroupIndex; int adIndexInAdGroup = id.adIndexInAdGroup; - Uri adUri = adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]; - if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { + Uri adUri = + Assertions.checkNotNull(adPlaybackState.adGroups[adGroupIndex].uris[adIndexInAdGroup]); + if (adMediaSourceHolders[adGroupIndex].length <= adIndexInAdGroup) { + int adCount = adIndexInAdGroup + 1; + adMediaSourceHolders[adGroupIndex] = + Arrays.copyOf(adMediaSourceHolders[adGroupIndex], adCount); + } + @Nullable + AdMediaSourceHolder adMediaSourceHolder = + adMediaSourceHolders[adGroupIndex][adIndexInAdGroup]; + if (adMediaSourceHolder == null) { MediaSource adMediaSource = adMediaSourceFactory.createMediaSource(adUri); - int oldAdCount = adGroupMediaSources[adGroupIndex].length; - if (adIndexInAdGroup >= oldAdCount) { - int adCount = adIndexInAdGroup + 1; - adGroupMediaSources[adGroupIndex] = - Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); - adGroupTimelines[adGroupIndex] = Arrays.copyOf(adGroupTimelines[adGroupIndex], adCount); - } - adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList<>()); + adMediaSourceHolder = new AdMediaSourceHolder(adMediaSource); + adMediaSourceHolders[adGroupIndex][adIndexInAdGroup] = adMediaSourceHolder; prepareChildSource(id, adMediaSource); } - MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - DeferredMediaPeriod deferredMediaPeriod = - new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs); - deferredMediaPeriod.setPrepareErrorListener( - new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); - List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); - if (mediaPeriods == null) { - Object periodUid = - adGroupTimelines[adGroupIndex][adIndexInAdGroup].getUidOfPeriod(/* periodIndex= */ 0); - MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); - deferredMediaPeriod.createPeriod(adSourceMediaPeriodId); - } else { - // Keep track of the deferred media period so it can be populated with the real media period - // when the source's info becomes available. - mediaPeriods.add(deferredMediaPeriod); - } - return deferredMediaPeriod; + return adMediaSourceHolder.createMediaPeriod(adUri, id, allocator, startPositionUs); } else { - DeferredMediaPeriod mediaPeriod = - new DeferredMediaPeriod(contentMediaSource, id, allocator, startPositionUs); + MaskingMediaPeriod mediaPeriod = + new MaskingMediaPeriod(contentMediaSource, id, allocator, startPositionUs); mediaPeriod.createPeriod(id); return mediaPeriod; } @@ -266,46 +229,49 @@ public final class AdsMediaSource extends CompositeMediaSource { @Override public void releasePeriod(MediaPeriod mediaPeriod) { - DeferredMediaPeriod deferredMediaPeriod = (DeferredMediaPeriod) mediaPeriod; - List mediaPeriods = - deferredMediaPeriodByAdMediaSource.get(deferredMediaPeriod.mediaSource); - if (mediaPeriods != null) { - mediaPeriods.remove(deferredMediaPeriod); + MaskingMediaPeriod maskingMediaPeriod = (MaskingMediaPeriod) mediaPeriod; + MediaPeriodId id = maskingMediaPeriod.id; + if (id.isAd()) { + AdMediaSourceHolder adMediaSourceHolder = + Assertions.checkNotNull(adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup]); + adMediaSourceHolder.releaseMediaPeriod(maskingMediaPeriod); + if (adMediaSourceHolder.isInactive()) { + releaseChildSource(id); + adMediaSourceHolders[id.adGroupIndex][id.adIndexInAdGroup] = null; + } + } else { + maskingMediaPeriod.releasePeriod(); } - deferredMediaPeriod.releasePeriod(); } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { super.releaseSourceInternal(); - componentListener.release(); + Assertions.checkNotNull(componentListener).release(); componentListener = null; - deferredMediaPeriodByAdMediaSource.clear(); contentTimeline = null; - contentManifest = null; adPlaybackState = null; - adGroupMediaSources = new MediaSource[0][]; - adGroupTimelines = new Timeline[0][]; + adMediaSourceHolders = new AdMediaSourceHolder[0][]; mainHandler.post(adsLoader::stop); } @Override protected void onChildSourceInfoRefreshed( - MediaPeriodId mediaPeriodId, - MediaSource mediaSource, - Timeline timeline, - @Nullable Object manifest) { + MediaPeriodId mediaPeriodId, MediaSource mediaSource, Timeline timeline) { if (mediaPeriodId.isAd()) { int adGroupIndex = mediaPeriodId.adGroupIndex; int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup; - onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline); + Assertions.checkNotNull(adMediaSourceHolders[adGroupIndex][adIndexInAdGroup]) + .handleSourceInfoRefresh(timeline); } else { - onContentSourceInfoRefreshed(timeline, manifest); + Assertions.checkArgument(timeline.getPeriodCount() == 1); + contentTimeline = timeline; } + maybeUpdateSourceInfo(); } @Override - protected @Nullable MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( + protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId( MediaPeriodId childId, MediaPeriodId mediaPeriodId) { // The child id for the content period is just DUMMY_CONTENT_MEDIA_PERIOD_ID. That's why we need // to forward the reported mediaPeriodId in this case. @@ -316,62 +282,35 @@ public final class AdsMediaSource extends CompositeMediaSource { private void onAdPlaybackState(AdPlaybackState adPlaybackState) { if (this.adPlaybackState == null) { - adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; - Arrays.fill(adGroupMediaSources, new MediaSource[0]); - adGroupTimelines = new Timeline[adPlaybackState.adGroupCount][]; - Arrays.fill(adGroupTimelines, new Timeline[0]); + adMediaSourceHolders = new AdMediaSourceHolder[adPlaybackState.adGroupCount][]; + Arrays.fill(adMediaSourceHolders, new AdMediaSourceHolder[0]); } this.adPlaybackState = adPlaybackState; maybeUpdateSourceInfo(); } - private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); - contentTimeline = timeline; - contentManifest = manifest; - maybeUpdateSourceInfo(); - } - - private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, - int adIndexInAdGroup, Timeline timeline) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); - adGroupTimelines[adGroupIndex][adIndexInAdGroup] = timeline; - List mediaPeriods = deferredMediaPeriodByAdMediaSource.remove(mediaSource); - if (mediaPeriods != null) { - Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); - for (int i = 0; i < mediaPeriods.size(); i++) { - DeferredMediaPeriod mediaPeriod = mediaPeriods.get(i); - MediaPeriodId adSourceMediaPeriodId = - new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); - mediaPeriod.createPeriod(adSourceMediaPeriodId); - } - } - maybeUpdateSourceInfo(); - } - private void maybeUpdateSourceInfo() { + @Nullable Timeline contentTimeline = this.contentTimeline; if (adPlaybackState != null && contentTimeline != null) { - adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurations(adGroupTimelines, period)); + adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs()); Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState); - refreshSourceInfo(timeline, contentManifest); + refreshSourceInfo(timeline); } } - private static long[][] getAdDurations(Timeline[][] adTimelines, Timeline.Period period) { - long[][] adDurations = new long[adTimelines.length][]; - for (int i = 0; i < adTimelines.length; i++) { - adDurations[i] = new long[adTimelines[i].length]; - for (int j = 0; j < adTimelines[i].length; j++) { - adDurations[i][j] = - adTimelines[i][j] == null - ? C.TIME_UNSET - : adTimelines[i][j].getPeriod(/* periodIndex= */ 0, period).getDurationUs(); + private long[][] getAdDurationsUs() { + long[][] adDurationsUs = new long[adMediaSourceHolders.length][]; + for (int i = 0; i < adMediaSourceHolders.length; i++) { + adDurationsUs[i] = new long[adMediaSourceHolders[i].length]; + for (int j = 0; j < adMediaSourceHolders[i].length; j++) { + @Nullable AdMediaSourceHolder holder = adMediaSourceHolders[i][j]; + adDurationsUs[i][j] = holder == null ? C.TIME_UNSET : holder.getDurationUs(); } } - return adDurations; + return adDurationsUs; } /** Listener for component events. All methods are called on the main thread. */ @@ -420,7 +359,7 @@ public final class AdsMediaSource extends CompositeMediaSource { dataSpec.uri, /* responseHeaders= */ Collections.emptyMap(), C.DATA_TYPE_AD, - C.TRACK_TYPE_UNKNOWN, + /* elapsedRealtimeMs= */ SystemClock.elapsedRealtime(), /* loadDurationMs= */ 0, /* bytesLoaded= */ 0, error, @@ -428,7 +367,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } } - private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener { + private final class AdPrepareErrorListener implements MaskingMediaPeriod.PrepareErrorListener { private final Uri adUri; private final int adGroupIndex; @@ -457,4 +396,61 @@ public final class AdsMediaSource extends CompositeMediaSource { () -> adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception)); } } + + private final class AdMediaSourceHolder { + + private final MediaSource adMediaSource; + private final List activeMediaPeriods; + + @MonotonicNonNull private Timeline timeline; + + public AdMediaSourceHolder(MediaSource adMediaSource) { + this.adMediaSource = adMediaSource; + activeMediaPeriods = new ArrayList<>(); + } + + public MediaPeriod createMediaPeriod( + Uri adUri, MediaPeriodId id, Allocator allocator, long startPositionUs) { + MaskingMediaPeriod maskingMediaPeriod = + new MaskingMediaPeriod(adMediaSource, id, allocator, startPositionUs); + maskingMediaPeriod.setPrepareErrorListener( + new AdPrepareErrorListener(adUri, id.adGroupIndex, id.adIndexInAdGroup)); + activeMediaPeriods.add(maskingMediaPeriod); + if (timeline != null) { + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); + MediaPeriodId adSourceMediaPeriodId = new MediaPeriodId(periodUid, id.windowSequenceNumber); + maskingMediaPeriod.createPeriod(adSourceMediaPeriodId); + } + return maskingMediaPeriod; + } + + public void handleSourceInfoRefresh(Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + if (this.timeline == null) { + Object periodUid = timeline.getUidOfPeriod(/* periodIndex= */ 0); + for (int i = 0; i < activeMediaPeriods.size(); i++) { + MaskingMediaPeriod mediaPeriod = activeMediaPeriods.get(i); + MediaPeriodId adSourceMediaPeriodId = + new MediaPeriodId(periodUid, mediaPeriod.id.windowSequenceNumber); + mediaPeriod.createPeriod(adSourceMediaPeriodId); + } + } + this.timeline = timeline; + } + + public long getDurationUs() { + return timeline == null + ? C.TIME_UNSET + : timeline.getPeriod(/* periodIndex= */ 0, period).getDurationUs(); + } + + public void releaseMediaPeriod(MaskingMediaPeriod maskingMediaPeriod) { + activeMediaPeriods.remove(maskingMediaPeriod); + maskingMediaPeriod.releasePeriod(); + } + + public boolean isInactive() { + return activeMediaPeriods.isEmpty(); + } + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java index 25a1440c8..cc82510a2 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -44,24 +44,16 @@ public final class SinglePeriodAdTimeline extends ForwardingTimeline { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { timeline.getPeriod(periodIndex, period, setIds); + long durationUs = + period.durationUs == C.TIME_UNSET ? adPlaybackState.contentDurationUs : period.durationUs; period.set( period.id, period.uid, period.windowIndex, - period.durationUs, + durationUs, period.getPositionInWindowUs(), adPlaybackState); return period; } - @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { - window = super.getWindow(windowIndex, window, setTag, defaultPositionProjectionUs); - if (window.durationUs == C.TIME_UNSET) { - window.durationUs = adPlaybackState.contentDurationUs; - } - return window; - } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index 68322c60a..74d8ddad3 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; @@ -58,7 +59,7 @@ public abstract class BaseMediaChunk extends MediaChunk { DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long clippedStartTimeUs, diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index 0bcc46fcb..a23e506d0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -21,7 +21,10 @@ import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; import com.google.android.exoplayer2.util.Log; -/** An output for {@link BaseMediaChunk}s. */ +/** + * A {@link TrackOutputProvider} that provides {@link TrackOutput TrackOutputs} based on a + * predefined mapping from track type to output. + */ public final class BaseMediaChunkOutput implements TrackOutputProvider { private static final String TAG = "BaseMediaChunkOutput"; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java index 2e7581eba..a794f67fe 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/Chunk.java @@ -56,7 +56,7 @@ public abstract class Chunk implements Loadable { * Optional data associated with the selection of the track to which this chunk belongs. Null if * the chunk does not belong to a track. */ - public final @Nullable Object trackSelectionData; + @Nullable public final Object trackSelectionData; /** * The start time of the media contained by the chunk, or {@link C#TIME_UNSET} if the data * being loaded does not contain media samples. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index fc07a318b..c4c8647a5 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.source.chunk; -import androidx.annotation.Nullable; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.DummyTrackOutput; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java index 6b7f5688a..d6400c516 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkHolder.java @@ -15,15 +15,15 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; + /** * Holds a chunk or an indication that the end of the stream has been reached. */ public final class ChunkHolder { - /** - * The chunk. - */ - public Chunk chunk; + /** The chunk. */ + @Nullable public Chunk chunk; /** * Indicates that the end of the stream has been reached. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 4cb9922c2..e2278d7f9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -15,18 +15,20 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.os.Looper; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction; @@ -60,8 +62,8 @@ public class ChunkSampleStream implements SampleStream, S public final int primaryTrackType; - private final int[] embeddedTrackTypes; - private final Format[] embeddedTrackFormats; + @Nullable private final int[] embeddedTrackTypes; + @Nullable private final Format[] embeddedTrackFormats; private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; @@ -73,10 +75,10 @@ public class ChunkSampleStream implements SampleStream, S private final List readOnlyMediaChunks; private final SampleQueue primarySampleQueue; private final SampleQueue[] embeddedSampleQueues; - private final BaseMediaChunkOutput mediaChunkOutput; + private final BaseMediaChunkOutput chunkOutput; private Format primaryDownstreamTrackFormat; - private @Nullable ReleaseCallback releaseCallback; + @Nullable private ReleaseCallback releaseCallback; private long pendingResetPositionUs; private long lastSeekPositionUs; private int nextNotifyPrimaryFormatMediaChunkIndex; @@ -95,57 +97,20 @@ public class ChunkSampleStream implements SampleStream, S * @param callback An {@link Callback} for the stream. * @param allocator An {@link Allocator} from which allocations can be obtained. * @param positionUs The position from which to start loading media. - * @param minLoadableRetryCount The minimum number of times that the source should retry a load - * before propagating an error. - * @param eventDispatcher A dispatcher to notify of events. - * @deprecated Use {@link #ChunkSampleStream(int, int[], Format[], ChunkSource, Callback, - * Allocator, long, LoadErrorHandlingPolicy, EventDispatcher)} instead. - */ - @Deprecated - public ChunkSampleStream( - int primaryTrackType, - int[] embeddedTrackTypes, - Format[] embeddedTrackFormats, - T chunkSource, - Callback> callback, - Allocator allocator, - long positionUs, - int minLoadableRetryCount, - EventDispatcher eventDispatcher) { - this( - primaryTrackType, - embeddedTrackTypes, - embeddedTrackFormats, - chunkSource, - callback, - allocator, - positionUs, - new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), - eventDispatcher); - } - - /** - * Constructs an instance. - * - * @param primaryTrackType The type of the primary track. One of the {@link C} {@code - * TRACK_TYPE_*} constants. - * @param embeddedTrackTypes The types of any embedded tracks, or null. - * @param embeddedTrackFormats The formats of the embedded tracks, or null. - * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. - * @param callback An {@link Callback} for the stream. - * @param allocator An {@link Allocator} from which allocations can be obtained. - * @param positionUs The position from which to start loading media. + * @param drmSessionManager The {@link DrmSessionManager} to obtain {@link DrmSession DrmSessions} + * from. * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ public ChunkSampleStream( int primaryTrackType, - int[] embeddedTrackTypes, - Format[] embeddedTrackFormats, + @Nullable int[] embeddedTrackTypes, + @Nullable Format[] embeddedTrackFormats, T chunkSource, Callback> callback, Allocator allocator, long positionUs, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; @@ -166,18 +131,25 @@ public class ChunkSampleStream implements SampleStream, S int[] trackTypes = new int[1 + embeddedTrackCount]; SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - primarySampleQueue = new SampleQueue(allocator); + primarySampleQueue = new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + drmSessionManager); trackTypes[0] = primaryTrackType; sampleQueues[0] = primarySampleQueue; for (int i = 0; i < embeddedTrackCount; i++) { - SampleQueue sampleQueue = new SampleQueue(allocator); + SampleQueue sampleQueue = + new SampleQueue( + allocator, + /* playbackLooper= */ Assertions.checkNotNull(Looper.myLooper()), + DrmSessionManager.getDummyDrmSessionManager()); embeddedSampleQueues[i] = sampleQueue; sampleQueues[i + 1] = sampleQueue; trackTypes[i + 1] = embeddedTrackTypes[i]; } - mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); + chunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); pendingResetPositionUs = positionUs; lastSeekPositionUs = positionUs; } @@ -220,8 +192,7 @@ public class ChunkSampleStream implements SampleStream, S if (embeddedTrackTypes[i] == trackType) { Assertions.checkState(!embeddedTracksSelected[i]); embeddedTracksSelected[i] = true; - embeddedSampleQueues[i].rewind(); - embeddedSampleQueues[i].advanceTo(positionUs, true, true); + embeddedSampleQueues[i].seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); return new EmbeddedSampleStream(this, embeddedSampleQueues[i], i); } } @@ -301,21 +272,16 @@ public class ChunkSampleStream implements SampleStream, S // See if we can seek inside the primary sample queue. boolean seekInsideBuffer; - primarySampleQueue.rewind(); if (seekToMediaChunk != null) { // When seeking to the start of a chunk we use the index of the first sample in the chunk // rather than the seek position. This ensures we seek to the keyframe at the start of the // chunk even if the sample timestamps are slightly offset from the chunk start times. - seekInsideBuffer = - primarySampleQueue.setReadPosition(seekToMediaChunk.getFirstSampleIndex(0)); + seekInsideBuffer = primarySampleQueue.seekTo(seekToMediaChunk.getFirstSampleIndex(0)); decodeOnlyUntilPositionUs = 0; } else { seekInsideBuffer = - primarySampleQueue.advanceTo( - positionUs, - /* toKeyframe= */ true, - /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()) - != SampleQueue.ADVANCE_FAILED; + primarySampleQueue.seekTo( + positionUs, /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()); decodeOnlyUntilPositionUs = lastSeekPositionUs; } @@ -324,10 +290,9 @@ public class ChunkSampleStream implements SampleStream, S nextNotifyPrimaryFormatMediaChunkIndex = primarySampleIndexToMediaChunkIndex( primarySampleQueue.getReadIndex(), /* minChunkIndex= */ 0); - // Advance the embedded sample queues to the seek position. + // Seek the embedded sample queues. for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.rewind(); - embeddedSampleQueue.advanceTo(positionUs, true, false); + embeddedSampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true); } } else { // We can't seek inside the buffer, and so need to reset. @@ -369,18 +334,18 @@ public class ChunkSampleStream implements SampleStream, S public void release(@Nullable ReleaseCallback callback) { this.releaseCallback = callback; // Discard as much as we can synchronously. - primarySampleQueue.discardToEnd(); + primarySampleQueue.preRelease(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.discardToEnd(); + embeddedSampleQueue.preRelease(); } loader.release(this); } @Override public void onLoaderReleased() { - primarySampleQueue.reset(); + primarySampleQueue.release(); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.reset(); + embeddedSampleQueue.release(); } if (releaseCallback != null) { releaseCallback.onSampleStreamReleased(this); @@ -391,12 +356,13 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && primarySampleQueue.hasNextSample()); + return !isPendingReset() && primarySampleQueue.isReady(loadingFinished); } @Override public void maybeThrowError() throws IOException { loader.maybeThrowError(); + primarySampleQueue.maybeThrowError(); if (!loader.isLoading()) { chunkSource.maybeThrowError(); } @@ -409,6 +375,7 @@ public class ChunkSampleStream implements SampleStream, S return C.RESULT_NOTHING_READ; } maybeNotifyPrimaryTrackFormatChanged(); + return primarySampleQueue.read( formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); } @@ -422,10 +389,7 @@ public class ChunkSampleStream implements SampleStream, S if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { skipCount = primarySampleQueue.advanceToEnd(); } else { - skipCount = primarySampleQueue.advanceTo(positionUs, true, true); - if (skipCount == SampleQueue.ADVANCE_FAILED) { - skipCount = 0; - } + skipCount = primarySampleQueue.advanceTo(positionUs); } maybeNotifyPrimaryTrackFormatChanged(); return skipCount; @@ -587,8 +551,10 @@ public class ChunkSampleStream implements SampleStream, S decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs; pendingResetPositionUs = C.TIME_UNSET; } - mediaChunk.init(mediaChunkOutput); + mediaChunk.init(chunkOutput); mediaChunks.add(mediaChunk); + } else if (loadable instanceof InitializationChunk) { + ((InitializationChunk) loadable).init(chunkOutput); } long elapsedRealtimeMs = loader.startLoading( @@ -606,6 +572,11 @@ public class ChunkSampleStream implements SampleStream, S return true; } + @Override + public boolean isLoading() { + return loader.isLoading(); + } + @Override public long getNextLoadPositionUs() { if (isPendingReset()) { @@ -768,7 +739,7 @@ public class ChunkSampleStream implements SampleStream, S @Override public boolean isReady() { - return loadingFinished || (!isPendingReset() && sampleQueue.hasNextSample()); + return !isPendingReset() && sampleQueue.isReady(loadingFinished); } @Override @@ -781,10 +752,7 @@ public class ChunkSampleStream implements SampleStream, S if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { skipCount = sampleQueue.advanceToEnd(); } else { - skipCount = sampleQueue.advanceTo(positionUs, true, true); - if (skipCount == SampleQueue.ADVANCE_FAILED) { - skipCount = 0; - } + skipCount = sampleQueue.advanceTo(positionUs); } return skipCount; } @@ -802,7 +770,11 @@ public class ChunkSampleStream implements SampleStream, S } maybeNotifyDownstreamFormat(); return sampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); + formatHolder, + buffer, + formatRequired, + loadingFinished, + decodeOnlyUntilPositionUs); } public void release() { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java index ee940954b..b119cad5b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -102,7 +102,10 @@ public interface ChunkSource { * @param e The error. * @param blacklistDurationMs The duration for which the associated track may be blacklisted, or * {@link C#TIME_UNSET} if the track may not be blacklisted. - * @return Whether the load should be canceled. Must be false if {@code cancelable} is false. + * @return Whether the load should be canceled so that a replacement chunk can be loaded instead. + * Must be {@code false} if {@code cancelable} is {@code false}. If {@code true}, {@link + * #getNextChunk(long, long, List, ChunkHolder)} will be called to obtain the replacement + * chunk. */ boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e, long blacklistDurationMs); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index ba7c0d0d5..9dffe0919 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; @@ -111,22 +112,21 @@ public class ContainerMediaChunk extends BaseMediaChunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override public final void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + if (nextLoadPosition == 0) { + // Configure the output and set it as the target for the extractor wrapper. + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(sampleOffsetUs); + extractorWrapper.init( + getTrackOutputProvider(output), + clippedStartTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedStartTimeUs - sampleOffsetUs), + clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs)); + } try { // Create and open the input. - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (nextLoadPosition == 0) { - // Configure the output and set it as the target for the extractor wrapper. - BaseMediaChunkOutput output = getOutput(); - output.setSampleOffsetUs(sampleOffsetUs); - extractorWrapper.init( - getTrackOutputProvider(output), - clippedStartTimeUs == C.TIME_UNSET - ? C.TIME_UNSET - : (clippedStartTimeUs - sampleOffsetUs), - clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs)); - } + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + ExtractorInput input = + new DefaultExtractorInput( + dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); // Load and decode the sample data. try { Extractor extractor = extractorWrapper.extractor; @@ -145,16 +145,13 @@ public class ContainerMediaChunk extends BaseMediaChunk { } /** - * Returns the {@link ChunkExtractorWrapper.TrackOutputProvider} to be used by the wrapped - * extractor. + * Returns the {@link TrackOutputProvider} to be used by the wrapped extractor. * * @param baseMediaChunkOutput The {@link BaseMediaChunkOutput} most recently passed to {@link * #init(BaseMediaChunkOutput)}. - * @return A {@link ChunkExtractorWrapper.TrackOutputProvider} to be used by the wrapped - * extractor. + * @return A {@link TrackOutputProvider} to be used by the wrapped extractor. */ - protected ChunkExtractorWrapper.TrackOutputProvider getTrackOutputProvider( - BaseMediaChunkOutput baseMediaChunkOutput) { + protected TrackOutputProvider getTrackOutputProvider(BaseMediaChunkOutput baseMediaChunkOutput) { return baseMediaChunkOutput; } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java index 7ea2521eb..f3bea8aeb 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; @@ -44,8 +45,14 @@ public abstract class DataChunk extends Chunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param data An optional recycled array that can be used as a holder for the data. */ - public DataChunk(DataSource dataSource, DataSpec dataSpec, int type, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, byte[] data) { + public DataChunk( + DataSource dataSource, + DataSpec dataSpec, + int type, + Format trackFormat, + int trackSelectionReason, + @Nullable Object trackSelectionData, + byte[] data) { super(dataSource, dataSpec, type, trackFormat, trackSelectionReason, trackSelectionData, C.TIME_UNSET, C.TIME_UNSET); this.data = data; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index 37c70d549..178fb94c7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -22,11 +22,13 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. @@ -37,6 +39,7 @@ public final class InitializationChunk extends Chunk { private final ChunkExtractorWrapper extractorWrapper; + @MonotonicNonNull private TrackOutputProvider trackOutputProvider; private long nextLoadPosition; private volatile boolean loadCanceled; @@ -60,6 +63,17 @@ public final class InitializationChunk extends Chunk { this.extractorWrapper = extractorWrapper; } + /** + * Initializes the chunk for loading, setting a {@link TrackOutputProvider} for track outputs to + * which formats will be written as they are loaded. + * + * @param trackOutputProvider The {@link TrackOutputProvider} for track outputs to which formats + * will be written as they are loaded. + */ + public void init(TrackOutputProvider trackOutputProvider) { + this.trackOutputProvider = trackOutputProvider; + } + // Loadable implementation. @Override @@ -70,17 +84,16 @@ public final class InitializationChunk extends Chunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + if (nextLoadPosition == 0) { + extractorWrapper.init( + trackOutputProvider, /* startTimeUs= */ C.TIME_UNSET, /* endTimeUs= */ C.TIME_UNSET); + } try { // Create and open the input. - ExtractorInput input = new DefaultExtractorInput(dataSource, - loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (nextLoadPosition == 0) { - extractorWrapper.init( - /* trackOutputProvider= */ null, - /* startTimeUs= */ C.TIME_UNSET, - /* endTimeUs= */ C.TIME_UNSET); - } + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + ExtractorInput input = + new DefaultExtractorInput( + dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); // Load and decode the initialization data. try { Extractor extractor = extractorWrapper.extractor; @@ -96,5 +109,4 @@ public final class InitializationChunk extends Chunk { Util.closeQuietly(dataSource); } } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java index 9626f4b03..39c097826 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.upstream.DataSource; @@ -44,7 +45,7 @@ public abstract class MediaChunk extends Chunk { DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long chunkIndex) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index d53caf8e1..00d841eee 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -91,19 +91,19 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { - DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); + BaseMediaChunkOutput output = getOutput(); + output.setSampleOffsetUs(0); + TrackOutput trackOutput = output.track(0, trackType); + trackOutput.format(sampleFormat); try { // Create and open the input. + DataSpec loadDataSpec = dataSpec.subrange(nextLoadPosition); long length = dataSource.open(loadDataSpec); if (length != C.LENGTH_UNSET) { length += nextLoadPosition; } ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, nextLoadPosition, length); - BaseMediaChunkOutput output = getOutput(); - output.setSampleOffsetUs(0); - TrackOutput trackOutput = output.track(0, trackType); - trackOutput.format(sampleFormat); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { @@ -117,5 +117,4 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { } loadCompleted = true; } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 8635005bf..fa8e5338f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -15,13 +15,16 @@ */ package com.google.android.exoplayer2.source.dash; +import android.util.Pair; +import android.util.SparseArray; +import android.util.SparseIntArray; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.util.Pair; -import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; @@ -58,6 +61,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** A DASH {@link MediaPeriod}. */ /* package */ final class DashMediaPeriod @@ -69,7 +73,8 @@ import java.util.regex.Pattern; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long elapsedRealtimeOffsetMs; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -82,7 +87,7 @@ import java.util.regex.Pattern; trackEmsgHandlerBySampleStream; private final EventDispatcher eventDispatcher; - private @Nullable Callback callback; + @Nullable private Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; private SequenceableLoader compositeSequenceableLoader; @@ -97,6 +102,7 @@ import java.util.regex.Pattern; int periodIndex, DashChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, long elapsedRealtimeOffsetMs, @@ -109,6 +115,7 @@ import java.util.regex.Pattern; this.periodIndex = periodIndex; this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; @@ -123,8 +130,8 @@ import java.util.regex.Pattern; compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); Period period = manifest.getPeriod(periodIndex); eventStreams = period.eventStreams; - Pair result = buildTrackGroups(period.adaptationSets, - eventStreams); + Pair result = + buildTrackGroups(drmSessionManager, period.adaptationSets, eventStreams); trackGroups = result.first; trackGroupInfos = result.second; eventDispatcher.mediaPeriodCreated(); @@ -219,9 +226,8 @@ import java.util.regex.Pattern; int totalTracksInPreviousAdaptationSets = 0; int tracksInCurrentAdaptationSet = manifestAdaptationSets.get(adaptationSetIndices[0]).representations.size(); - for (int i = 0; i < trackIndices.length; i++) { - while (trackIndices[i] - >= totalTracksInPreviousAdaptationSets + tracksInCurrentAdaptationSet) { + for (int trackIndex : trackIndices) { + while (trackIndex >= totalTracksInPreviousAdaptationSets + tracksInCurrentAdaptationSet) { currentAdaptationSetIndex++; totalTracksInPreviousAdaptationSets += tracksInCurrentAdaptationSet; tracksInCurrentAdaptationSet = @@ -234,15 +240,19 @@ import java.util.regex.Pattern; new StreamKey( periodIndex, adaptationSetIndices[currentAdaptationSetIndex], - trackIndices[i] - totalTracksInPreviousAdaptationSets)); + trackIndex - totalTracksInPreviousAdaptationSets)); } } return streamKeys; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections); releaseDisabledStreams(selections, mayRetainStreamFlags, streams); releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex); @@ -288,6 +298,11 @@ import java.util.regex.Pattern; return compositeSequenceableLoader.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); + } + @Override public long getNextLoadPositionUs() { return compositeSequenceableLoader.getNextLoadPositionUs(); @@ -466,7 +481,9 @@ import java.util.regex.Pattern; } private static Pair buildTrackGroups( - List adaptationSets, List eventStreams) { + DrmSessionManager drmSessionManager, + List adaptationSets, + List eventStreams) { int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); int primaryGroupCount = groupedAdaptationSetIndices.length; @@ -486,6 +503,7 @@ import java.util.regex.Pattern; int trackGroupCount = buildPrimaryAndEmbeddedTrackGroupInfos( + drmSessionManager, adaptationSets, groupedAdaptationSetIndices, primaryGroupCount, @@ -499,51 +517,94 @@ import java.util.regex.Pattern; return Pair.create(new TrackGroupArray(trackGroups), trackGroupInfos); } + /** + * Groups adaptation sets. Two adaptations sets belong to the same group if either: + * + *

      + *
    • One is a trick-play adaptation set and uses a {@code + * http://dashif.org/guidelines/trickmode} essential or supplemental property to indicate + * that the other is the main adaptation set to which it corresponds. + *
    • The two adaptation sets are marked as safe for switching using {@code + * urn:mpeg:dash:adaptation-set-switching:2016} supplemental properties. + *
    + * + * @param adaptationSets The adaptation sets to merge. + * @return An array of groups, where each group is an array of adaptation set indices. + */ private static int[][] getGroupedAdaptationSetIndices(List adaptationSets) { int adaptationSetCount = adaptationSets.size(); - SparseIntArray idToIndexMap = new SparseIntArray(adaptationSetCount); + SparseIntArray adaptationSetIdToIndex = new SparseIntArray(adaptationSetCount); + List> adaptationSetGroupedIndices = new ArrayList<>(adaptationSetCount); + SparseArray> adaptationSetIndexToGroupedIndices = + new SparseArray<>(adaptationSetCount); + + // Initially make each adaptation set belong to its own group. Also build the + // adaptationSetIdToIndex map. for (int i = 0; i < adaptationSetCount; i++) { - idToIndexMap.put(adaptationSets.get(i).id, i); + adaptationSetIdToIndex.put(adaptationSets.get(i).id, i); + List initialGroup = new ArrayList<>(); + initialGroup.add(i); + adaptationSetGroupedIndices.add(initialGroup); + adaptationSetIndexToGroupedIndices.put(i, initialGroup); } - int[][] groupedAdaptationSetIndices = new int[adaptationSetCount][]; - boolean[] adaptationSetUsedFlags = new boolean[adaptationSetCount]; - - int groupCount = 0; + // Merge adaptation set groups. for (int i = 0; i < adaptationSetCount; i++) { - if (adaptationSetUsedFlags[i]) { - // This adaptation set has already been included in a group. - continue; + int mergedGroupIndex = i; + AdaptationSet adaptationSet = adaptationSets.get(i); + + // Trick-play adaptation sets are merged with their corresponding main adaptation sets. + @Nullable + Descriptor trickPlayProperty = findTrickPlayProperty(adaptationSet.essentialProperties); + if (trickPlayProperty == null) { + // Trick-play can also be specified using a supplemental property. + trickPlayProperty = findTrickPlayProperty(adaptationSet.supplementalProperties); } - adaptationSetUsedFlags[i] = true; - Descriptor adaptationSetSwitchingProperty = findAdaptationSetSwitchingProperty( - adaptationSets.get(i).supplementalProperties); - if (adaptationSetSwitchingProperty == null) { - groupedAdaptationSetIndices[groupCount++] = new int[] {i}; - } else { - String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ","); - int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; - adaptationSetIndices[0] = i; - int outputIndex = 1; - for (int j = 0; j < extraAdaptationSetIds.length; j++) { - int extraIndex = - idToIndexMap.get( - Integer.parseInt(extraAdaptationSetIds[j]), /* valueIfKeyNotFound= */ -1); - if (extraIndex != -1) { - adaptationSetUsedFlags[extraIndex] = true; - adaptationSetIndices[outputIndex] = extraIndex; - outputIndex++; + if (trickPlayProperty != null) { + int mainAdaptationSetId = Integer.parseInt(trickPlayProperty.value); + int mainAdaptationSetIndex = + adaptationSetIdToIndex.get(mainAdaptationSetId, /* valueIfKeyNotFound= */ -1); + if (mainAdaptationSetIndex != -1) { + mergedGroupIndex = mainAdaptationSetIndex; + } + } + + // Adaptation sets that are safe for switching are merged, using the smallest index for the + // merged group. + if (mergedGroupIndex == i) { + @Nullable + Descriptor adaptationSetSwitchingProperty = + findAdaptationSetSwitchingProperty(adaptationSet.supplementalProperties); + if (adaptationSetSwitchingProperty != null) { + String[] otherAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ","); + for (String adaptationSetId : otherAdaptationSetIds) { + int otherAdaptationSetId = + adaptationSetIdToIndex.get( + Integer.parseInt(adaptationSetId), /* valueIfKeyNotFound= */ -1); + if (otherAdaptationSetId != -1) { + mergedGroupIndex = Math.min(mergedGroupIndex, otherAdaptationSetId); + } } } - if (outputIndex < adaptationSetIndices.length) { - adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex); - } - groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices; + } + + // Merge the groups if necessary. + if (mergedGroupIndex != i) { + List thisGroup = adaptationSetIndexToGroupedIndices.get(i); + List mergedGroup = adaptationSetIndexToGroupedIndices.get(mergedGroupIndex); + mergedGroup.addAll(thisGroup); + adaptationSetIndexToGroupedIndices.put(i, mergedGroup); + adaptationSetGroupedIndices.remove(thisGroup); } } - return groupCount < adaptationSetCount - ? Arrays.copyOf(groupedAdaptationSetIndices, groupCount) : groupedAdaptationSetIndices; + int[][] groupedAdaptationSetIndices = new int[adaptationSetGroupedIndices.size()][]; + for (int i = 0; i < groupedAdaptationSetIndices.length; i++) { + groupedAdaptationSetIndices[i] = Util.toArray(adaptationSetGroupedIndices.get(i)); + // Restore the original adaptation set order within each group. + Arrays.sort(groupedAdaptationSetIndices[i]); + } + return groupedAdaptationSetIndices; } /** @@ -581,6 +642,7 @@ import java.util.regex.Pattern; } private static int buildPrimaryAndEmbeddedTrackGroupInfos( + DrmSessionManager drmSessionManager, List adaptationSets, int[][] groupedAdaptationSetIndices, int primaryGroupCount, @@ -597,7 +659,14 @@ import java.util.regex.Pattern; } Format[] formats = new Format[representations.size()]; for (int j = 0; j < formats.length; j++) { - formats[j] = representations.get(j).format; + Format format = representations.get(j).format; + DrmInitData drmInitData = format.drmInitData; + if (drmInitData != null) { + format = + format.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(drmInitData)); + } + formats[j] = format; } AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); @@ -704,6 +773,7 @@ import java.util.regex.Pattern; this, allocator, positionUs, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher); synchronized (this) { @@ -714,9 +784,19 @@ import java.util.regex.Pattern; } private static Descriptor findAdaptationSetSwitchingProperty(List descriptors) { + return findDescriptor(descriptors, "urn:mpeg:dash:adaptation-set-switching:2016"); + } + + @Nullable + private static Descriptor findTrickPlayProperty(List descriptors) { + return findDescriptor(descriptors, "http://dashif.org/guidelines/trickmode"); + } + + @Nullable + private static Descriptor findDescriptor(List descriptors, String schemeIdUri) { for (int i = 0; i < descriptors.size(); i++) { Descriptor descriptor = descriptors.get(i); - if ("urn:mpeg:dash:adaptation-set-switching:2016".equals(descriptor.schemeIdUri)) { + if (schemeIdUri.equals(descriptor.schemeIdUri)) { return descriptor; } } @@ -793,7 +873,8 @@ import java.util.regex.Pattern; /* initializationData= */ null); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index b83a4af4f..39cc03dd1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -18,13 +18,15 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -34,8 +36,8 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SequenceableLoader; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; @@ -74,11 +76,12 @@ public final class DashMediaSource extends BaseMediaSource { } /** Factory for {@link DashMediaSource}s. */ - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final DashChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; + private DrmSessionManager drmSessionManager; @Nullable private ParsingLoadable.Parser manifestParser; @Nullable private List streamKeys; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -112,6 +115,7 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); @@ -126,7 +130,7 @@ public final class DashMediaSource extends BaseMediaSource { * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; @@ -213,19 +217,6 @@ public final class DashMediaSource extends BaseMediaSource { return this; } - /** - * Sets a list of {@link StreamKey stream keys} by which the manifest is filtered. - * - * @param streamKeys A list of {@link StreamKey stream keys}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; - } - /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source * loads data from multiple streams (video, audio etc...). The default is an instance of {@link @@ -266,6 +257,7 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, @@ -288,6 +280,40 @@ public final class DashMediaSource extends BaseMediaSource { return mediaSource; } + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public DashMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + DashMediaSource mediaSource = createMediaSource(manifestUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + @Override + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); + return this; + } + /** * Returns a new {@link DashMediaSource} using the current parameters. * @@ -310,26 +336,18 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, livePresentationDelayOverridesManifest, tag); } - /** - * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, - * MediaSourceEventListener)} instead. - */ - @Deprecated - public DashMediaSource createMediaSource( - Uri manifestUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { - DashMediaSource mediaSource = createMediaSource(manifestUri); - if (eventHandler != null && eventListener != null) { - mediaSource.addEventListener(eventHandler, eventListener); - } - return mediaSource; + @Override + public Factory setStreamKeys(List streamKeys) { + Assertions.checkState(!isCreateCalled); + this.streamKeys = streamKeys; + return this; } @Override @@ -352,8 +370,8 @@ public final class DashMediaSource extends BaseMediaSource { /** * The interval in milliseconds between invocations of {@link - * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the - * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). + * MediaSourceCaller#onSourceInfoRefreshed(MediaSource, Timeline)} when the source's {@link + * Timeline} is changing dynamically (for example, for incomplete live streams). */ private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; /** @@ -367,6 +385,7 @@ public final class DashMediaSource extends BaseMediaSource { private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final boolean livePresentationDelayOverridesManifest; @@ -379,11 +398,11 @@ public final class DashMediaSource extends BaseMediaSource { private final Runnable simulateManifestRefreshRunnable; private final PlayerEmsgCallback playerEmsgCallback; private final LoaderErrorThrower manifestLoadErrorThrower; - private final @Nullable Object tag; + @Nullable private final Object tag; private DataSource dataSource; private Loader loader; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private IOException manifestFatalError; private Handler handler; @@ -415,8 +434,8 @@ public final class DashMediaSource extends BaseMediaSource { public DashMediaSource( DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, chunkSourceFactory, @@ -440,8 +459,8 @@ public final class DashMediaSource extends BaseMediaSource { DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, /* manifestUri= */ null, @@ -449,6 +468,7 @@ public final class DashMediaSource extends BaseMediaSource { /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* livePresentationDelayOverridesManifest= */ false, @@ -476,8 +496,8 @@ public final class DashMediaSource extends BaseMediaSource { Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -513,8 +533,8 @@ public final class DashMediaSource extends BaseMediaSource { DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -553,8 +573,8 @@ public final class DashMediaSource extends BaseMediaSource { DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( /* manifest= */ null, manifestUri, @@ -562,6 +582,7 @@ public final class DashMediaSource extends BaseMediaSource { manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS ? DEFAULT_LIVE_PRESENTATION_DELAY_MS @@ -574,12 +595,13 @@ public final class DashMediaSource extends BaseMediaSource { } private DashMediaSource( - DashManifest manifest, - Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, - ParsingLoadable.Parser manifestParser, + @Nullable DashManifest manifest, + @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + @Nullable ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, boolean livePresentationDelayOverridesManifest, @@ -590,6 +612,7 @@ public final class DashMediaSource extends BaseMediaSource { this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.livePresentationDelayMs = livePresentationDelayMs; this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest; @@ -636,8 +659,9 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; + drmSessionManager.prepare(); if (sideloadedManifest) { processManifest(false); } else { @@ -666,6 +690,7 @@ public final class DashMediaSource extends BaseMediaSource { periodIndex, chunkSourceFactory, mediaTransferListener, + drmSessionManager, loadErrorHandlingPolicy, periodEventDispatcher, elapsedRealtimeOffsetMs, @@ -685,7 +710,7 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { manifestLoadPending = false; dataSource = null; if (loader != null) { @@ -706,6 +731,7 @@ public final class DashMediaSource extends BaseMediaSource { expiredManifestPublishTimeUs = C.TIME_UNSET; firstPeriodId = 0; periodsById.clear(); + drmSessionManager.release(); } // PlayerEmsgCallback callbacks. @@ -784,15 +810,18 @@ public final class DashMediaSource extends BaseMediaSource { manifestLoadPending &= manifest.dynamic; manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; manifestLoadEndTimestampMs = elapsedRealtimeMs; - if (manifest.location != null) { - synchronized (manifestUriLock) { - // This condition checks that replaceManifestUri wasn't called between the start and end of - // this load. If it was, we ignore the manifest location and prefer the manual replacement. - @SuppressWarnings("ReferenceEquality") - boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; - if (isSameUriInstance) { - manifestUri = manifest.location; - } + + synchronized (manifestUriLock) { + // Checks whether replaceManifestUri(Uri) was called to manually replace the URI between the + // start and end of this load. If it was then isSameUriInstance evaluates to false, and we + // prefer the manual replacement to one derived from the previous request. + @SuppressWarnings("ReferenceEquality") + boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; + if (isSameUriInstance) { + // Replace the manifest URI with one specified by a manifest Location element (if present), + // or with the final (possibly redirected) URI. This follows the recommendation in + // DASH-IF-IOP 4.3, section 3.2.15.3. See: https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf. + manifestUri = manifest.location != null ? manifest.location : loadable.getUri(); } } @@ -988,8 +1017,13 @@ public final class DashMediaSource extends BaseMediaSource { windowDurationUs / 2); } } - long windowStartTimeMs = manifest.availabilityStartTimeMs - + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs); + long windowStartTimeMs = C.TIME_UNSET; + if (manifest.availabilityStartTimeMs != C.TIME_UNSET) { + windowStartTimeMs = + manifest.availabilityStartTimeMs + + manifest.getPeriod(0).startMs + + C.usToMs(currentStartTimeUs); + } DashTimeline timeline = new DashTimeline( manifest.availabilityStartTimeMs, @@ -1000,7 +1034,7 @@ public final class DashMediaSource extends BaseMediaSource { windowDefaultStartPositionUs, manifest, tag); - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); if (!sideloadedManifest) { // Remove any pending simulated refresh. @@ -1148,7 +1182,7 @@ public final class DashMediaSource extends BaseMediaSource { private final long windowDurationUs; private final long windowDefaultStartPositionUs; private final DashManifest manifest; - private final @Nullable Object windowTag; + @Nullable private final Object windowTag; public DashTimeline( long presentationStartTimeMs, @@ -1190,22 +1224,19 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( defaultPositionProjectionUs); - Object tag = setTag ? windowTag : null; - boolean isDynamic = - manifest.dynamic - && manifest.minUpdatePeriodMs != C.TIME_UNSET - && manifest.durationMs == C.TIME_UNSET; return window.set( - tag, + Window.SINGLE_WINDOW_UID, + windowTag, + manifest, presentationStartTimeMs, windowStartTimeMs, /* isSeekable= */ true, - isDynamic, + /* isDynamic= */ isMovingLiveWindow(manifest), + /* isLive= */ manifest.dynamic, windowDefaultStartPositionUs, windowDurationUs, /* firstPeriodIndex= */ 0, @@ -1225,7 +1256,7 @@ public final class DashMediaSource extends BaseMediaSource { private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) { long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (!manifest.dynamic) { + if (!isMovingLiveWindow(manifest)) { return windowDefaultStartPositionUs; } if (defaultPositionProjectionUs > 0) { @@ -1270,6 +1301,12 @@ public final class DashMediaSource extends BaseMediaSource { Assertions.checkIndex(periodIndex, 0, getPeriodCount()); return firstPeriodId + periodIndex; } + + private static boolean isMovingLiveWindow(DashManifest manifest) { + return manifest.dynamic + && manifest.minUpdatePeriodMs != C.TIME_UNSET + && manifest.durationMs == C.TIME_UNSET; + } } private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java index 6a6e08ce1..c9433b9e4 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DashUtil.java @@ -66,7 +66,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable DrmInitData loadDrmInitData(DataSource dataSource, Period period) + @Nullable + public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) throws IOException, InterruptedException { int primaryTrackType = C.TRACK_TYPE_VIDEO; Representation representation = getFirstRepresentation(period, primaryTrackType); @@ -95,7 +96,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable Format loadSampleFormat( + @Nullable + public static Format loadSampleFormat( DataSource dataSource, int trackType, Representation representation) throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, @@ -116,7 +118,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static @Nullable ChunkIndex loadChunkIndex( + @Nullable + public static ChunkIndex loadChunkIndex( DataSource dataSource, int trackType, Representation representation) throws IOException, InterruptedException { ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, @@ -138,7 +141,8 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - private static @Nullable ChunkExtractorWrapper loadInitializationData( + @Nullable + private static ChunkExtractorWrapper loadInitializationData( DataSource dataSource, int trackType, Representation representation, boolean loadIndex) throws IOException, InterruptedException { RangedUri initializationUri = representation.getInitializationUri(); @@ -187,7 +191,8 @@ public final class DashUtil { return new ChunkExtractorWrapper(extractor, trackType, format); } - private static @Nullable Representation getFirstRepresentation(Period period, int type) { + @Nullable + private static Representation getFirstRepresentation(Period period, int type) { int index = period.getAdaptationSetIndex(type); if (index == C.INDEX_UNSET) { return null; @@ -197,5 +202,4 @@ public final class DashUtil { } private DashUtil() {} - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 95e0199d3..cf0ffde41 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -67,7 +67,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private final int maxSegmentsPerLoad; public Factory(DataSource.Factory dataSourceFactory) { - this(dataSourceFactory, 1); + this(dataSourceFactory, /* maxSegmentsPerLoad= */ 1); } public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) { @@ -622,7 +622,7 @@ public class DefaultDashChunkSource implements DashChunkSource { /* package */ final @Nullable ChunkExtractorWrapper extractorWrapper; public final Representation representation; - public final @Nullable DashSegmentIndex segmentIndex; + @Nullable public final DashSegmentIndex segmentIndex; private final long periodDurationUs; private final long segmentNumShift; @@ -633,7 +633,7 @@ public class DefaultDashChunkSource implements DashChunkSource { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput) { this( periodDurationUs, representation, @@ -795,7 +795,7 @@ public class DefaultDashChunkSource implements DashChunkSource { Representation representation, boolean enableEventMessageTrack, List closedCaptionFormats, - TrackOutput playerEmsgTrackOutput) { + @Nullable TrackOutput playerEmsgTrackOutput) { String containerMimeType = representation.format.containerMimeType; if (mimeTypeIsRawText(containerMimeType)) { return null; @@ -812,10 +812,12 @@ public class DefaultDashChunkSource implements DashChunkSource { } extractor = new FragmentedMp4Extractor( - flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput); + flags, + /* timestampAdjuster= */ null, + /* sideloadedTrack= */ null, + closedCaptionFormats, + playerEmsgTrackOutput); } - // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, - // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. return new ChunkExtractorWrapper(extractor, trackType, representation.format); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java index f06a70996..6e67be6ec 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java @@ -115,9 +115,9 @@ import java.io.IOException; byte[] serializedEvent = eventMessageEncoder.encode(eventStream.events[sampleIndex]); if (serializedEvent != null) { buffer.ensureSpaceForWrite(serializedEvent.length); - buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME); buffer.data.put(serializedEvent); buffer.timeUs = eventTimesUs[sampleIndex]; + buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME); return C.RESULT_BUFFER_READ; } else { return C.RESULT_NOTHING_READ; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java index d11ccdece..187baad76 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.metadata.Metadata; @@ -195,7 +196,7 @@ public final class PlayerEmsgHandler implements Handler.Callback { /** Returns a {@link TrackOutput} that emsg messages could be written to. */ public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() { - return new PlayerTrackEmsgHandler(new SampleQueue(allocator)); + return new PlayerTrackEmsgHandler(allocator); } /** Release this emsg handler. It should not be reused after this call. */ @@ -282,9 +283,11 @@ public final class PlayerEmsgHandler implements Handler.Callback { private final FormatHolder formatHolder; private final MetadataInputBuffer buffer; - /* package */ PlayerTrackEmsgHandler(SampleQueue sampleQueue) { - this.sampleQueue = sampleQueue; - + /* package */ PlayerTrackEmsgHandler(Allocator allocator) { + this.sampleQueue = new SampleQueue( + allocator, + /* playbackLooper= */ handler.getLooper(), + DrmSessionManager.getDummyDrmSessionManager()); formatHolder = new FormatHolder(); buffer = new MetadataInputBuffer(); } @@ -347,22 +350,19 @@ public final class PlayerEmsgHandler implements Handler.Callback { /** Release this track emsg handler. It should not be reused after this call. */ public void release() { - sampleQueue.reset(); + sampleQueue.release(); } // Internal methods. private void parseAndDiscardSamples() { - while (sampleQueue.hasNextSample()) { + while (sampleQueue.isReady(/* loadingFinished= */ false)) { MetadataInputBuffer inputBuffer = dequeueSample(); if (inputBuffer == null) { continue; } long eventTimeUs = inputBuffer.timeUs; Metadata metadata = decoder.decode(inputBuffer); - if (metadata == null) { - continue; - } EventMessage eventMessage = (EventMessage) metadata.get(0); if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { parsePlayerEmsgEvent(eventTimeUs, eventMessage); @@ -374,7 +374,13 @@ public final class PlayerEmsgHandler implements Handler.Callback { @Nullable private MetadataInputBuffer dequeueSample() { buffer.clear(); - int result = sampleQueue.read(formatHolder, buffer, false, false, 0); + int result = + sampleQueue.read( + formatHolder, + buffer, + /* formatRequired= */ false, + /* loadingFinished= */ false, + /* decodeOnlyUntilUs= */ 0); if (result == C.RESULT_BUFFER_READ) { buffer.flip(); return buffer; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java index d96237474..b0689eeb1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java @@ -50,9 +50,10 @@ public class AdaptationSet { */ public final List accessibilityDescriptors; - /** - * Supplemental properties in the adaptation set. - */ + /** Essential properties in the adaptation set. */ + public final List essentialProperties; + + /** Supplemental properties in the adaptation set. */ public final List supplementalProperties; /** @@ -62,21 +63,21 @@ public class AdaptationSet { * {@code TRACK_TYPE_*} constants. * @param representations {@link Representation}s in the adaptation set. * @param accessibilityDescriptors Accessibility descriptors in the adaptation set. + * @param essentialProperties Essential properties in the adaptation set. * @param supplementalProperties Supplemental properties in the adaptation set. */ - public AdaptationSet(int id, int type, List representations, - List accessibilityDescriptors, List supplementalProperties) { + public AdaptationSet( + int id, + int type, + List representations, + List accessibilityDescriptors, + List essentialProperties, + List supplementalProperties) { this.id = id; this.type = type; this.representations = Collections.unmodifiableList(representations); - this.accessibilityDescriptors = - accessibilityDescriptors == null - ? Collections.emptyList() - : Collections.unmodifiableList(accessibilityDescriptors); - this.supplementalProperties = - supplementalProperties == null - ? Collections.emptyList() - : Collections.unmodifiableList(supplementalProperties); + this.accessibilityDescriptors = Collections.unmodifiableList(accessibilityDescriptors); + this.essentialProperties = Collections.unmodifiableList(essentialProperties); + this.supplementalProperties = Collections.unmodifiableList(supplementalProperties); } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java index 0c3f641cb..c21af45d1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java @@ -80,12 +80,10 @@ public class DashManifest implements FilterableManifest { * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section * 4.7.2. */ - public final UtcTimingElement utcTiming; + @Nullable public final UtcTimingElement utcTiming; - /** - * The location of this manifest. - */ - public final Uri location; + /** The location of this manifest, or null if not present. */ + @Nullable public final Uri location; /** The {@link ProgramInformation}, or null if not present. */ @Nullable public final ProgramInformation programInformation; @@ -106,8 +104,8 @@ public class DashManifest implements FilterableManifest { long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, - UtcTimingElement utcTiming, - Uri location, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { this( availabilityStartTimeMs, @@ -134,8 +132,8 @@ public class DashManifest implements FilterableManifest { long suggestedPresentationDelayMs, long publishTimeMs, @Nullable ProgramInformation programInformation, - UtcTimingElement utcTiming, - Uri location, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; this.durationMs = durationMs; @@ -226,9 +224,14 @@ public class DashManifest implements FilterableManifest { key = keys.poll(); } while (key.periodIndex == periodIndex && key.groupIndex == adaptationSetIndex); - copyAdaptationSets.add(new AdaptationSet(adaptationSet.id, adaptationSet.type, - copyRepresentations, adaptationSet.accessibilityDescriptors, - adaptationSet.supplementalProperties)); + copyAdaptationSets.add( + new AdaptationSet( + adaptationSet.id, + adaptationSet.type, + copyRepresentations, + adaptationSet.accessibilityDescriptors, + adaptationSet.essentialProperties, + adaptationSet.supplementalProperties)); } while(key.periodIndex == periodIndex); // Add back the last key which doesn't belong to the period being processed keys.addFirst(key); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 95e8c7f94..6d25c50cf 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -20,6 +20,7 @@ import android.text.TextUtils; import android.util.Base64; import android.util.Pair; import android.util.Xml; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -47,6 +48,7 @@ import java.util.List; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -101,7 +103,7 @@ public class DashManifestParser extends DefaultHandler long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET); String typeString = xpp.getAttributeValue(null, "type"); - boolean dynamic = typeString != null && "dynamic".equals(typeString); + boolean dynamic = "dynamic".equals(typeString); long minUpdateTimeMs = dynamic ? parseDuration(xpp, "minimumUpdatePeriod", C.TIME_UNSET) : C.TIME_UNSET; long timeShiftBufferDepthMs = dynamic @@ -189,9 +191,9 @@ public class DashManifestParser extends DefaultHandler long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, - ProgramInformation programInformation, - UtcTimingElement utcTiming, - Uri location, + @Nullable ProgramInformation programInformation, + @Nullable UtcTimingElement utcTiming, + @Nullable Uri location, List periods) { return new DashManifest( availabilityStartTime, @@ -220,10 +222,11 @@ public class DashManifestParser extends DefaultHandler protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs) throws XmlPullParserException, IOException { - String id = xpp.getAttributeValue(null, "id"); + @Nullable String id = xpp.getAttributeValue(null, "id"); long startMs = parseDuration(xpp, "start", defaultStartMs); long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); - SegmentBase segmentBase = null; + @Nullable SegmentBase segmentBase = null; + @Nullable Descriptor assetIdentifier = null; List adaptationSets = new ArrayList<>(); List eventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; @@ -235,32 +238,40 @@ public class DashManifestParser extends DefaultHandler seenFirstBaseUrl = true; } } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase)); + adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase, durationMs)); } else if (XmlPullParserUtil.isStartTag(xpp, "EventStream")) { eventStreams.add(parseEventStream(xpp)); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, null); + segmentBase = parseSegmentList(xpp, null, durationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList()); + segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) { + assetIdentifier = parseDescriptor(xpp, "AssetIdentifier"); } else { maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "Period")); - return Pair.create(buildPeriod(id, startMs, adaptationSets, eventStreams), durationMs); + return Pair.create( + buildPeriod(id, startMs, adaptationSets, eventStreams, assetIdentifier), durationMs); } - protected Period buildPeriod(String id, long startMs, List adaptationSets, - List eventStreams) { - return new Period(id, startMs, adaptationSets, eventStreams); + protected Period buildPeriod( + @Nullable String id, + long startMs, + List adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { + return new Period(id, startMs, adaptationSets, eventStreams, assetIdentifier); } // AdaptationSet parsing. - protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, - SegmentBase segmentBase) throws XmlPullParserException, IOException { + protected AdaptationSet parseAdaptationSet( + XmlPullParser xpp, String baseUrl, @Nullable SegmentBase segmentBase, long periodDurationMs) + throws XmlPullParserException, IOException { int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET); int contentType = parseContentType(xpp); @@ -278,6 +289,7 @@ public class DashManifestParser extends DefaultHandler ArrayList inbandEventStreams = new ArrayList<>(); ArrayList accessibilityDescriptors = new ArrayList<>(); ArrayList roleDescriptors = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(); ArrayList supplementalProperties = new ArrayList<>(); List representationInfos = new ArrayList<>(); @@ -306,6 +318,8 @@ public class DashManifestParser extends DefaultHandler audioChannels = parseAudioChannelConfiguration(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) { accessibilityDescriptors.add(parseDescriptor(xpp, "Accessibility")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) { @@ -323,18 +337,21 @@ public class DashManifestParser extends DefaultHandler language, roleDescriptors, accessibilityDescriptors, + essentialProperties, supplementalProperties, - segmentBase); + segmentBase, + periodDurationMs); contentType = checkContentTypeConsistency(contentType, getContentType(representationInfo.format)); representationInfos.add(representationInfo); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); + segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase, periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = - parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase, supplementalProperties); + parseSegmentTemplate( + xpp, (SegmentTemplate) segmentBase, supplementalProperties, periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp, "Label")) { @@ -356,14 +373,28 @@ public class DashManifestParser extends DefaultHandler inbandEventStreams)); } - return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, + return buildAdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, supplementalProperties); } - protected AdaptationSet buildAdaptationSet(int id, int contentType, - List representations, List accessibilityDescriptors, + protected AdaptationSet buildAdaptationSet( + int id, + int contentType, + List representations, + List accessibilityDescriptors, + List essentialProperties, List supplementalProperties) { - return new AdaptationSet(id, contentType, representations, accessibilityDescriptors, + return new AdaptationSet( + id, + contentType, + representations, + accessibilityDescriptors, + essentialProperties, supplementalProperties); } @@ -399,13 +430,12 @@ public class DashManifestParser extends DefaultHandler * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element. * Either or both may be null, depending on the ContentProtection element being parsed. */ - protected Pair parseContentProtection(XmlPullParser xpp) - throws XmlPullParserException, IOException { + protected Pair<@NullableType String, @NullableType SchemeData> parseContentProtection( + XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeType = null; String licenseServerUrl = null; byte[] data = null; UUID uuid = null; - boolean requiresSecureDecoder = false; String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); if (schemeIdUri != null) { @@ -439,9 +469,6 @@ public class DashManifestParser extends DefaultHandler xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) { licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl"); - } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { - String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); - requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } else if (data == null && XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh") && xpp.next() == XmlPullParser.TEXT) { @@ -465,10 +492,7 @@ public class DashManifestParser extends DefaultHandler } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); SchemeData schemeData = - uuid != null - ? new SchemeData( - uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) - : null; + uuid != null ? new SchemeData(uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data) : null; return Pair.create(schemeType, schemeData); } @@ -489,18 +513,20 @@ public class DashManifestParser extends DefaultHandler protected RepresentationInfo parseRepresentation( XmlPullParser xpp, String baseUrl, - String adaptationSetMimeType, - String adaptationSetCodecs, + @Nullable String adaptationSetMimeType, + @Nullable String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, int adaptationSetAudioSamplingRate, - String adaptationSetLanguage, + @Nullable String adaptationSetLanguage, List adaptationSetRoleDescriptors, List adaptationSetAccessibilityDescriptors, + List adaptationSetEssentialProperties, List adaptationSetSupplementalProperties, - SegmentBase segmentBase) + @Nullable SegmentBase segmentBase, + long periodDurationMs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE); @@ -515,7 +541,9 @@ public class DashManifestParser extends DefaultHandler String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); - ArrayList supplementalProperties = new ArrayList<>(); + ArrayList essentialProperties = new ArrayList<>(adaptationSetEssentialProperties); + ArrayList supplementalProperties = + new ArrayList<>(adaptationSetSupplementalProperties); boolean seenFirstBaseUrl = false; do { @@ -530,11 +558,14 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase); + segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase, periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate( - xpp, (SegmentTemplate) segmentBase, adaptationSetSupplementalProperties); + xpp, + (SegmentTemplate) segmentBase, + adaptationSetSupplementalProperties, + periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { Pair contentProtection = parseContentProtection(xpp); if (contentProtection.first != null) { @@ -545,6 +576,8 @@ public class DashManifestParser extends DefaultHandler } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "EssentialProperty")) { + essentialProperties.add(parseDescriptor(xpp, "EssentialProperty")); } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } else { @@ -566,6 +599,7 @@ public class DashManifestParser extends DefaultHandler adaptationSetRoleDescriptors, adaptationSetAccessibilityDescriptors, codecs, + essentialProperties, supplementalProperties); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); @@ -574,23 +608,26 @@ public class DashManifestParser extends DefaultHandler } protected Format buildFormat( - String id, - String containerMimeType, + @Nullable String id, + @Nullable String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, - String language, + @Nullable String language, List roleDescriptors, List accessibilityDescriptors, - String codecs, + @Nullable String codecs, + List essentialProperties, List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); @C.SelectionFlags int selectionFlags = parseSelectionFlagsFromRoleDescriptors(roleDescriptors); @C.RoleFlags int roleFlags = parseRoleFlagsFromRoleDescriptors(roleDescriptors); roleFlags |= parseRoleFlagsFromAccessibilityDescriptors(accessibilityDescriptors); + roleFlags |= parseRoleFlagsFromProperties(essentialProperties); + roleFlags |= parseRoleFlagsFromProperties(supplementalProperties); if (sampleMimeType != null) { if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); @@ -661,8 +698,8 @@ public class DashManifestParser extends DefaultHandler protected Representation buildRepresentation( RepresentationInfo representationInfo, - String label, - String extraDrmSchemeType, + @Nullable String label, + @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format format = representationInfo.format; @@ -690,7 +727,8 @@ public class DashManifestParser extends DefaultHandler // SegmentBase, SegmentList and SegmentTemplate parsing. - protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, SingleSegmentBase parent) + protected SingleSegmentBase parseSegmentBase( + XmlPullParser xpp, @Nullable SingleSegmentBase parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -726,7 +764,8 @@ public class DashManifestParser extends DefaultHandler indexLength); } - protected SegmentList parseSegmentList(XmlPullParser xpp, SegmentList parent) + protected SegmentList parseSegmentList( + XmlPullParser xpp, @Nullable SegmentList parent, long periodDurationMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -744,7 +783,7 @@ public class DashManifestParser extends DefaultHandler if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { initialization = parseInitialization(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { - timeline = parseSegmentTimeline(xpp); + timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentURL")) { if (segments == null) { segments = new ArrayList<>(); @@ -771,16 +810,17 @@ public class DashManifestParser extends DefaultHandler long presentationTimeOffset, long startNumber, long duration, - List timeline, - List segments) { + @Nullable List timeline, + @Nullable List segments) { return new SegmentList(initialization, timescale, presentationTimeOffset, startNumber, duration, timeline, segments); } protected SegmentTemplate parseSegmentTemplate( XmlPullParser xpp, - SegmentTemplate parent, - List adaptationSetSupplementalProperties) + @Nullable SegmentTemplate parent, + List adaptationSetSupplementalProperties, + long periodDurationMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -803,7 +843,7 @@ public class DashManifestParser extends DefaultHandler if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { initialization = parseInitialization(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { - timeline = parseSegmentTimeline(xpp); + timeline = parseSegmentTimeline(xpp, timescale, periodDurationMs); } else { maybeSkipTag(xpp); } @@ -834,8 +874,8 @@ public class DashManifestParser extends DefaultHandler long endNumber, long duration, List timeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate) { return new SegmentTemplate( initialization, timescale, @@ -998,33 +1038,84 @@ public class DashManifestParser extends DefaultHandler return new EventMessage(schemeIdUri, value, durationMs, id, messageData); } - protected List parseSegmentTimeline(XmlPullParser xpp) + protected List parseSegmentTimeline( + XmlPullParser xpp, long timescale, long periodDurationMs) throws XmlPullParserException, IOException { List segmentTimeline = new ArrayList<>(); - long elapsedTime = 0; + long startTime = 0; + long elementDuration = C.TIME_UNSET; + int elementRepeatCount = 0; + boolean havePreviousTimelineElement = false; do { xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "S")) { - elapsedTime = parseLong(xpp, "t", elapsedTime); - long duration = parseLong(xpp, "d", C.TIME_UNSET); - int count = 1 + parseInt(xpp, "r", 0); - for (int i = 0; i < count; i++) { - segmentTimeline.add(buildSegmentTimelineElement(elapsedTime, duration)); - elapsedTime += duration; + long newStartTime = parseLong(xpp, "t", C.TIME_UNSET); + if (havePreviousTimelineElement) { + startTime = + addSegmentTimelineElementsToList( + segmentTimeline, + startTime, + elementDuration, + elementRepeatCount, + /* endTime= */ newStartTime); } + if (newStartTime != C.TIME_UNSET) { + startTime = newStartTime; + } + elementDuration = parseLong(xpp, "d", C.TIME_UNSET); + elementRepeatCount = parseInt(xpp, "r", 0); + havePreviousTimelineElement = true; } else { maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTimeline")); + if (havePreviousTimelineElement) { + long periodDuration = Util.scaleLargeTimestamp(periodDurationMs, timescale, 1000); + addSegmentTimelineElementsToList( + segmentTimeline, + startTime, + elementDuration, + elementRepeatCount, + /* endTime= */ periodDuration); + } return segmentTimeline; } - protected SegmentTimelineElement buildSegmentTimelineElement(long elapsedTime, long duration) { - return new SegmentTimelineElement(elapsedTime, duration); + /** + * Adds timeline elements for one S tag to the segment timeline. + * + * @param startTime Start time of the first timeline element. + * @param elementDuration Duration of one timeline element. + * @param elementRepeatCount Number of timeline elements minus one. May be negative to indicate + * that the count is determined by the total duration and the element duration. + * @param endTime End time of the last timeline element for this S tag, or {@link C#TIME_UNSET} if + * unknown. Only needed if {@code repeatCount} is negative. + * @return Calculated next start time. + */ + private long addSegmentTimelineElementsToList( + List segmentTimeline, + long startTime, + long elementDuration, + int elementRepeatCount, + long endTime) { + int count = + elementRepeatCount >= 0 + ? 1 + elementRepeatCount + : (int) Util.ceilDivide(endTime - startTime, elementDuration); + for (int i = 0; i < count; i++) { + segmentTimeline.add(buildSegmentTimelineElement(startTime, elementDuration)); + startTime += elementDuration; + } + return startTime; } - protected UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name, - UrlTemplate defaultValue) { + protected SegmentTimelineElement buildSegmentTimelineElement(long startTime, long duration) { + return new SegmentTimelineElement(startTime, duration); + } + + @Nullable + protected UrlTemplate parseUrlTemplate( + XmlPullParser xpp, String name, @Nullable UrlTemplate defaultValue) { String valueString = xpp.getAttributeValue(null, name); if (valueString != null) { return UrlTemplate.compile(valueString); @@ -1170,7 +1261,19 @@ public class DashManifestParser extends DefaultHandler } @C.RoleFlags - protected int parseDashRoleSchemeValue(String value) { + protected int parseRoleFlagsFromProperties(List accessibilityDescriptors) { + @C.RoleFlags int result = 0; + for (int i = 0; i < accessibilityDescriptors.size(); i++) { + Descriptor descriptor = accessibilityDescriptors.get(i); + if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) { + result |= C.ROLE_FLAG_TRICK_PLAY; + } + } + return result; + } + + @C.RoleFlags + protected int parseDashRoleSchemeValue(@Nullable String value) { if (value == null) { return 0; } @@ -1203,7 +1306,7 @@ public class DashManifestParser extends DefaultHandler } @C.RoleFlags - protected int parseTvaAudioPurposeCsValue(String value) { + protected int parseTvaAudioPurposeCsValue(@Nullable String value) { if (value == null) { return 0; } @@ -1274,7 +1377,9 @@ public class DashManifestParser extends DefaultHandler * @param codecs The codecs attribute. * @return The derived sample mimeType, or null if it could not be derived. */ - private static String getSampleMimeType(String containerMimeType, String codecs) { + @Nullable + private static String getSampleMimeType( + @Nullable String containerMimeType, @Nullable String codecs) { if (MimeTypes.isAudio(containerMimeType)) { return MimeTypes.getAudioMediaMimeType(codecs); } else if (MimeTypes.isVideo(containerMimeType)) { @@ -1308,7 +1413,7 @@ public class DashManifestParser extends DefaultHandler * @param mimeType The mimeType. * @return Whether the mimeType is a text sample mimeType. */ - private static boolean mimeTypeIsRawText(String mimeType) { + private static boolean mimeTypeIsRawText(@Nullable String mimeType) { return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType) || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) @@ -1317,16 +1422,18 @@ public class DashManifestParser extends DefaultHandler } /** - * Checks two languages for consistency, returning the consistent language, or throwing an - * {@link IllegalStateException} if the languages are inconsistent. - *

    - * Two languages are consistent if they are equal, or if one is null. + * Checks two languages for consistency, returning the consistent language, or throwing an {@link + * IllegalStateException} if the languages are inconsistent. + * + *

    Two languages are consistent if they are equal, or if one is null. * * @param firstLanguage The first language. * @param secondLanguage The second language. * @return The consistent language. */ - private static String checkLanguageConsistency(String firstLanguage, String secondLanguage) { + @Nullable + private static String checkLanguageConsistency( + @Nullable String firstLanguage, @Nullable String secondLanguage) { if (firstLanguage == null) { return secondLanguage; } else if (secondLanguage == null) { @@ -1417,8 +1524,10 @@ public class DashManifestParser extends DefaultHandler for (int i = 0; i < supplementalProperties.size(); i++) { Descriptor descriptor = supplementalProperties.get(i); String schemeIdUri = descriptor.schemeIdUri; - if ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) - && "ec+3".equals(descriptor.value)) { + if (("tag:dolby.com,2018:dash:EC3_ExtensionType:2018".equals(schemeIdUri) + && "JOC".equals(descriptor.value)) + || ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) + && "ec+3".equals(descriptor.value))) { return MimeTypes.AUDIO_E_AC3_JOC; } } @@ -1538,14 +1647,19 @@ public class DashManifestParser extends DefaultHandler public final Format format; public final String baseUrl; public final SegmentBase segmentBase; - public final String drmSchemeType; + @Nullable public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; public final long revisionId; - public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - String drmSchemeType, ArrayList drmSchemeDatas, - ArrayList inbandEventStreams, long revisionId) { + public RepresentationInfo( + Format format, + String baseUrl, + SegmentBase segmentBase, + @Nullable String drmSchemeType, + ArrayList drmSchemeDatas, + ArrayList inbandEventStreams, + long revisionId) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java index 493a8da09..d68690d36 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Descriptor.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source.dash.manifest; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; @@ -24,10 +23,8 @@ import com.google.android.exoplayer2.util.Util; */ public final class Descriptor { - /** - * The scheme URI. - */ - @NonNull public final String schemeIdUri; + /** The scheme URI. */ + public final String schemeIdUri; /** * The value, or null. */ @@ -42,7 +39,7 @@ public final class Descriptor { * @param value The value, or null. * @param id The identifier, or null. */ - public Descriptor(@NonNull String schemeIdUri, @Nullable String value, @Nullable String id) { + public Descriptor(String schemeIdUri, @Nullable String value, @Nullable String id) { this.schemeIdUri = schemeIdUri; this.value = value; this.id = id; @@ -63,10 +60,9 @@ public final class Descriptor { @Override public int hashCode() { - int result = (schemeIdUri != null ? schemeIdUri.hashCode() : 0); + int result = schemeIdUri.hashCode(); result = 31 * result + (value != null ? value.hashCode() : 0); result = 31 * result + (id != null ? id.hashCode() : 0); return result; } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java index 18614ca4b..b472aed50 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java @@ -45,13 +45,16 @@ public class Period { */ public final List eventStreams; + /** The asset identifier for this period, if one exists */ + @Nullable public final Descriptor assetIdentifier; + /** * @param id The period identifier. May be null. * @param startMs The start time of the period in milliseconds. * @param adaptationSets The adaptation sets belonging to the period. */ public Period(@Nullable String id, long startMs, List adaptationSets) { - this(id, startMs, adaptationSets, Collections.emptyList()); + this(id, startMs, adaptationSets, Collections.emptyList(), /* assetIdentifier= */ null); } /** @@ -62,10 +65,27 @@ public class Period { */ public Period(@Nullable String id, long startMs, List adaptationSets, List eventStreams) { + this(id, startMs, adaptationSets, eventStreams, /* assetIdentifier= */ null); + } + + /** + * @param id The period identifier. May be null. + * @param startMs The start time of the period in milliseconds. + * @param adaptationSets The adaptation sets belonging to the period. + * @param eventStreams The {@link EventStream}s belonging to the period. + * @param assetIdentifier The asset identifier for this period + */ + public Period( + @Nullable String id, + long startMs, + List adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { this.id = id; this.startMs = startMs; this.adaptationSets = Collections.unmodifiableList(adaptationSets); this.eventStreams = Collections.unmodifiableList(eventStreams); + this.assetIdentifier = assetIdentifier; } /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java index 62934d743..ac264bd2b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/ProgramInformation.java @@ -21,22 +21,26 @@ import com.google.android.exoplayer2.util.Util; /** A parsed program information element. */ public class ProgramInformation { /** The title for the media presentation. */ - public final String title; + @Nullable public final String title; /** Information about the original source of the media presentation. */ - public final String source; + @Nullable public final String source; /** A copyright statement for the media presentation. */ - public final String copyright; + @Nullable public final String copyright; /** A URL that provides more information about the media presentation. */ - public final String moreInformationURL; + @Nullable public final String moreInformationURL; /** Declares the language code(s) for this ProgramInformation. */ - public final String lang; + @Nullable public final String lang; public ProgramInformation( - String title, String source, String copyright, String moreInformationURL, String lang) { + @Nullable String title, + @Nullable String source, + @Nullable String copyright, + @Nullable String moreInformationURL, + @Nullable String lang) { this.title = title; this.source = source; this.copyright = copyright; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java index c7bb4adec..bcd783f0c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java @@ -83,10 +83,11 @@ public final class RangedUri { *

    If {@code other} is null then the merge is considered unsuccessful, and null is returned. * * @param other The {@link RangedUri} to merge. - * @param baseUri The optional base Uri. + * @param baseUri The base Uri. * @return The merged {@link RangedUri} if the merge was successful. Null otherwise. */ - public @Nullable RangedUri attemptMerge(@Nullable RangedUri other, String baseUri) { + @Nullable + public RangedUri attemptMerge(@Nullable RangedUri other, String baseUri) { final String resolvedUri = resolveUriString(baseUri); if (other == null || !resolvedUri.equals(other.resolveUriString(baseUri))) { return null; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java index 0884bcc65..80ad15cd8 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Representation.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; @@ -53,9 +54,7 @@ public abstract class Representation { * The offset of the presentation timestamps in the media stream relative to media time. */ public final long presentationTimeOffsetUs; - /** - * The in-band event streams in the representation. Never null, but may be empty. - */ + /** The in-band event streams in the representation. May be empty. */ public final List inbandEventStreams; private final RangedUri initializationUri; @@ -71,7 +70,7 @@ public abstract class Representation { */ public static Representation newInstance( long revisionId, Format format, String baseUrl, SegmentBase segmentBase) { - return newInstance(revisionId, format, baseUrl, segmentBase, null); + return newInstance(revisionId, format, baseUrl, segmentBase, /* inbandEventStreams= */ null); } /** @@ -89,8 +88,9 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams) { - return newInstance(revisionId, format, baseUrl, segmentBase, inbandEventStreams, null); + @Nullable List inbandEventStreams) { + return newInstance( + revisionId, format, baseUrl, segmentBase, inbandEventStreams, /* cacheKey= */ null); } /** @@ -110,8 +110,8 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams, - String cacheKey) { + @Nullable List inbandEventStreams, + @Nullable String cacheKey) { if (segmentBase instanceof SingleSegmentBase) { return new SingleSegmentRepresentation( revisionId, @@ -135,7 +135,7 @@ public abstract class Representation { Format format, String baseUrl, SegmentBase segmentBase, - List inbandEventStreams) { + @Nullable List inbandEventStreams) { this.revisionId = revisionId; this.format = format; this.baseUrl = baseUrl; @@ -151,6 +151,7 @@ public abstract class Representation { * Returns a {@link RangedUri} defining the location of the representation's initialization data, * or null if no initialization data exists. */ + @Nullable public RangedUri getInitializationUri() { return initializationUri; } @@ -159,14 +160,15 @@ public abstract class Representation { * Returns a {@link RangedUri} defining the location of the representation's segment index, or * null if the representation provides an index directly. */ + @Nullable public abstract RangedUri getIndexUri(); - /** - * Returns an index if the representation provides one directly, or null otherwise. - */ + /** Returns an index if the representation provides one directly, or null otherwise. */ + @Nullable public abstract DashSegmentIndex getIndex(); /** Returns a cache key for the representation if set, or null. */ + @Nullable public abstract String getCacheKey(); /** @@ -184,9 +186,9 @@ public abstract class Representation { */ public final long contentLength; - private final String cacheKey; - private final RangedUri indexUri; - private final SingleSegmentIndex segmentIndex; + @Nullable private final String cacheKey; + @Nullable private final RangedUri indexUri; + @Nullable private final SingleSegmentIndex segmentIndex; /** * @param revisionId Identifies the revision of the content. @@ -209,7 +211,7 @@ public abstract class Representation { long indexStart, long indexEnd, List inbandEventStreams, - String cacheKey, + @Nullable String cacheKey, long contentLength) { RangedUri rangedUri = new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1); @@ -233,8 +235,8 @@ public abstract class Representation { Format format, String baseUrl, SingleSegmentBase segmentBase, - List inbandEventStreams, - String cacheKey, + @Nullable List inbandEventStreams, + @Nullable String cacheKey, long contentLength) { super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.uri = Uri.parse(baseUrl); @@ -248,16 +250,19 @@ public abstract class Representation { } @Override + @Nullable public RangedUri getIndexUri() { return indexUri; } @Override + @Nullable public DashSegmentIndex getIndex() { return segmentIndex; } @Override + @Nullable public String getCacheKey() { return cacheKey; } @@ -284,12 +289,13 @@ public abstract class Representation { Format format, String baseUrl, MultiSegmentBase segmentBase, - List inbandEventStreams) { + @Nullable List inbandEventStreams) { super(revisionId, format, baseUrl, segmentBase, inbandEventStreams); this.segmentBase = segmentBase; } @Override + @Nullable public RangedUri getIndexUri() { return null; } @@ -300,6 +306,7 @@ public abstract class Representation { } @Override + @Nullable public String getCacheKey() { return null; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java index ba4faafd9..db7c8d647 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash.manifest; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.dash.DashSegmentIndex; import com.google.android.exoplayer2.util.Util; @@ -25,7 +26,7 @@ import java.util.List; */ public abstract class SegmentBase { - /* package */ final RangedUri initialization; + /* package */ @Nullable final RangedUri initialization; /* package */ final long timescale; /* package */ final long presentationTimeOffset; @@ -36,7 +37,8 @@ public abstract class SegmentBase { * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. */ - public SegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset) { + public SegmentBase( + @Nullable RangedUri initialization, long timescale, long presentationTimeOffset) { this.initialization = initialization; this.timescale = timescale; this.presentationTimeOffset = presentationTimeOffset; @@ -49,6 +51,7 @@ public abstract class SegmentBase { * @param representation The {@link Representation} for which initialization data is required. * @return A {@link RangedUri} defining the location of the initialization data, or null. */ + @Nullable public RangedUri getInitialization(Representation representation) { return initialization; } @@ -77,19 +80,31 @@ public abstract class SegmentBase { * @param indexStart The byte offset of the index data in the segment. * @param indexLength The length of the index data in bytes. */ - public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, - long indexStart, long indexLength) { + public SingleSegmentBase( + @Nullable RangedUri initialization, + long timescale, + long presentationTimeOffset, + long indexStart, + long indexLength) { super(initialization, timescale, presentationTimeOffset); this.indexStart = indexStart; this.indexLength = indexLength; } public SingleSegmentBase() { - this(null, 1, 0, 0, 0); + this( + /* initialization= */ null, + /* timescale= */ 1, + /* presentationTimeOffset= */ 0, + /* indexStart= */ 0, + /* indexLength= */ 0); } + @Nullable public RangedUri getIndex() { - return indexLength <= 0 ? null : new RangedUri(null, indexStart, indexLength); + return indexLength <= 0 + ? null + : new RangedUri(/* referenceUri= */ null, indexStart, indexLength); } } @@ -101,7 +116,7 @@ public abstract class SegmentBase { /* package */ final long startNumber; /* package */ final long duration; - /* package */ final List segmentTimeline; + /* package */ @Nullable final List segmentTimeline; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -118,12 +133,12 @@ public abstract class SegmentBase { * parameter. */ public MultiSegmentBase( - RangedUri initialization, + @Nullable RangedUri initialization, long timescale, long presentationTimeOffset, long startNumber, long duration, - List segmentTimeline) { + @Nullable List segmentTimeline) { super(initialization, timescale, presentationTimeOffset); this.startNumber = startNumber; this.duration = duration; @@ -223,7 +238,7 @@ public abstract class SegmentBase { */ public static class SegmentList extends MultiSegmentBase { - /* package */ final List mediaSegments; + /* package */ @Nullable final List mediaSegments; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -246,8 +261,8 @@ public abstract class SegmentBase { long presentationTimeOffset, long startNumber, long duration, - List segmentTimeline, - List mediaSegments) { + @Nullable List segmentTimeline, + @Nullable List mediaSegments) { super(initialization, timescale, presentationTimeOffset, startNumber, duration, segmentTimeline); this.mediaSegments = mediaSegments; @@ -275,8 +290,8 @@ public abstract class SegmentBase { */ public static class SegmentTemplate extends MultiSegmentBase { - /* package */ final UrlTemplate initializationTemplate; - /* package */ final UrlTemplate mediaTemplate; + /* package */ @Nullable final UrlTemplate initializationTemplate; + /* package */ @Nullable final UrlTemplate mediaTemplate; /* package */ final long endNumber; /** @@ -308,9 +323,9 @@ public abstract class SegmentBase { long startNumber, long endNumber, long duration, - List segmentTimeline, - UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate) { + @Nullable List segmentTimeline, + @Nullable UrlTemplate initializationTemplate, + @Nullable UrlTemplate mediaTemplate) { super( initialization, timescale, @@ -324,6 +339,7 @@ public abstract class SegmentBase { } @Override + @Nullable public RangedUri getInitialization(Representation representation) { if (initializationTemplate != null) { String urlString = initializationTemplate.buildUri(representation.format.id, 0, @@ -381,6 +397,22 @@ public abstract class SegmentBase { this.duration = duration; } + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SegmentTimelineElement that = (SegmentTimelineElement) o; + return startTime == that.startTime && duration == that.duration; + } + + @Override + public int hashCode() { + return 31 * (int) startTime + (int) duration; + } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java new file mode 100644 index 000000000..b7c267727 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/manifest/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.source.dash.manifest; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java new file mode 100644 index 000000000..4eb0d8436 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.source.dash.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java new file mode 100644 index 000000000..f51ea4369 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/dash/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.source.dash; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java index 4fe76cdf8..fe70298dc 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/Aes128DataSource.java @@ -51,7 +51,7 @@ import javax.crypto.spec.SecretKeySpec; private final byte[] encryptionKey; private final byte[] encryptionIv; - private @Nullable CipherInputStream cipherInputStream; + @Nullable private CipherInputStream cipherInputStream; /** * @param upstream The upstream {@link DataSource}. @@ -105,7 +105,8 @@ import javax.crypto.spec.SecretKeySpec; } @Override - public final @Nullable Uri getUri() { + @Nullable + public final Uri getUri() { return upstream.getUri(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 2ee241910..de4c425c7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -16,10 +16,9 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; @@ -85,11 +84,10 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { @Override public Result createExtractor( - Extractor previousExtractor, + @Nullable Extractor previousExtractor, Uri uri, Format format, - List muxedCaptionFormats, - DrmInitData drmInitData, + @Nullable List muxedCaptionFormats, TimestampAdjuster timestampAdjuster, Map> responseHeaders, ExtractorInput extractorInput) @@ -111,8 +109,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // Try selecting the extractor by the file extension. Extractor extractorByFileExtension = - createExtractorByFileExtension( - uri, format, muxedCaptionFormats, drmInitData, timestampAdjuster); + createExtractorByFileExtension(uri, format, muxedCaptionFormats, timestampAdjuster); extractorInput.resetPeekPosition(); if (sniffQuietly(extractorByFileExtension, extractorInput)) { return buildResult(extractorByFileExtension); @@ -159,7 +156,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { if (!(extractorByFileExtension instanceof FragmentedMp4Extractor)) { FragmentedMp4Extractor fragmentedMp4Extractor = - createFragmentedMp4Extractor(timestampAdjuster, format, drmInitData, muxedCaptionFormats); + createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); if (sniffQuietly(fragmentedMp4Extractor, extractorInput)) { return buildResult(fragmentedMp4Extractor); } @@ -185,8 +182,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { private Extractor createExtractorByFileExtension( Uri uri, Format format, - List muxedCaptionFormats, - DrmInitData drmInitData, + @Nullable List muxedCaptionFormats, TimestampAdjuster timestampAdjuster) { String lastPathSegment = uri.getLastPathSegment(); if (lastPathSegment == null) { @@ -209,8 +205,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5) || lastPathSegment.startsWith(CMF_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { - return createFragmentedMp4Extractor( - timestampAdjuster, format, drmInitData, muxedCaptionFormats); + return createFragmentedMp4Extractor(timestampAdjuster, format, muxedCaptionFormats); } else { // For any other file extension, we assume TS format. return createTsExtractor( @@ -226,7 +221,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { @DefaultTsPayloadReaderFactory.Flags int userProvidedPayloadReaderFactoryFlags, boolean exposeCea608WhenMissingDeclarations, Format format, - List muxedCaptionFormats, + @Nullable List muxedCaptionFormats, TimestampAdjuster timestampAdjuster) { @DefaultTsPayloadReaderFactory.Flags int payloadReaderFactoryFlags = @@ -270,26 +265,32 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { private static FragmentedMp4Extractor createFragmentedMp4Extractor( TimestampAdjuster timestampAdjuster, Format format, - DrmInitData drmInitData, @Nullable List muxedCaptionFormats) { - boolean isVariant = false; - for (int i = 0; i < format.metadata.length(); i++) { - Metadata.Entry entry = format.metadata.get(i); - if (entry instanceof HlsTrackMetadataEntry) { - isVariant = !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty(); - break; - } - } // Only enable the EMSG TrackOutput if this is the 'variant' track (i.e. the main one) to avoid // creating a separate EMSG track for every audio track in a video stream. return new FragmentedMp4Extractor( - /* flags= */ isVariant ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0, + /* flags= */ isFmp4Variant(format) ? FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK : 0, timestampAdjuster, /* sideloadedTrack= */ null, - drmInitData, muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } + /** Returns true if this {@code format} represents a 'variant' track (i.e. the main one). */ + private static boolean isFmp4Variant(Format format) { + Metadata metadata = format.metadata; + if (metadata == null) { + return false; + } + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof HlsTrackMetadataEntry) { + return !((HlsTrackMetadataEntry) entry).variantInfos.isEmpty(); + } + } + return false; + } + + @Nullable private static Result buildResultForSameExtractorType( Extractor previousExtractor, Format format, TimestampAdjuster timestampAdjuster) { if (previousExtractor instanceof WebvttExtractor) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java new file mode 100644 index 000000000..54ab3e295 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/FullSegmentEncryptionKeyCache.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls; + +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * LRU cache that holds up to {@code maxSize} full-segment-encryption keys. Which each addition, + * once the cache's size exceeds {@code maxSize}, the oldest item (according to insertion order) is + * removed. + */ +/* package */ final class FullSegmentEncryptionKeyCache { + + private final LinkedHashMap backingMap; + + public FullSegmentEncryptionKeyCache(int maxSize) { + backingMap = + new LinkedHashMap( + /* initialCapacity= */ maxSize + 1, /* loadFactor= */ 1, /* accessOrder= */ false) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > maxSize; + } + }; + } + + /** + * Returns the {@code encryptionKey} cached against this {@code uri}, or null if {@code uri} is + * null or not present in the cache. + */ + @Nullable + public byte[] get(@Nullable Uri uri) { + if (uri == null) { + return null; + } + return backingMap.get(uri); + } + + /** + * Inserts an entry into the cache. + * + * @throws NullPointerException if {@code uri} or {@code encryptionKey} are null. + */ + @Nullable + public byte[] put(Uri uri, byte[] encryptionKey) { + return backingMap.put(Assertions.checkNotNull(uri), Assertions.checkNotNull(encryptionKey)); + } + + /** + * Returns true if {@code uri} is present in the cache. + * + * @throws NullPointerException if {@code uri} is null. + */ + public boolean containsUri(Uri uri) { + return backingMap.containsKey(Assertions.checkNotNull(uri)); + } + + /** + * Removes {@code uri} from the cache. If {@code uri} was present in the cahce, this returns the + * corresponding {@code encryptionKey}, otherwise null. + * + * @throws NullPointerException if {@code uri} is null. + */ + @Nullable + public byte[] remove(Uri uri) { + return backingMap.remove(Assertions.checkNotNull(uri)); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index ee5a5f080..1a77715e7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -41,13 +41,10 @@ import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * Source of Hls (possibly adaptive) chunks. - */ +/** Source of Hls (possibly adaptive) chunks. */ /* package */ class HlsChunkSource { /** @@ -59,10 +56,8 @@ import java.util.Map; clear(); } - /** - * The chunk to be loaded next. - */ - public Chunk chunk; + /** The chunk to be loaded next. */ + @Nullable public Chunk chunk; /** * Indicates that the end of the stream has been reached. @@ -70,7 +65,7 @@ import java.util.Map; public boolean endOfStream; /** Indicates that the chunk source is waiting for the referred playlist to be refreshed. */ - public Uri playlistUrl; + @Nullable public Uri playlistUrl; /** * Clears the holder. @@ -97,13 +92,13 @@ import java.util.Map; private final Format[] playlistFormats; private final HlsPlaylistTracker playlistTracker; private final TrackGroup trackGroup; - private final List muxedCaptionFormats; + @Nullable private final List muxedCaptionFormats; private final FullSegmentEncryptionKeyCache keyCache; private boolean isTimestampMaster; private byte[] scratchSpace; - private IOException fatalError; - private Uri expectedPlaylistUrl; + @Nullable private IOException fatalError; + @Nullable private Uri expectedPlaylistUrl; private boolean independentSegments; // Note: The track group in the selection is typically *not* equal to trackGroup. This is due to @@ -138,14 +133,15 @@ import java.util.Map; HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, TimestampAdjusterProvider timestampAdjusterProvider, - List muxedCaptionFormats) { + @Nullable List muxedCaptionFormats) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.playlistUrls = playlistUrls; this.playlistFormats = playlistFormats; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; - keyCache = new FullSegmentEncryptionKeyCache(); + keyCache = new FullSegmentEncryptionKeyCache(KEY_CACHE_SIZE); + scratchSpace = Util.EMPTY_BYTE_ARRAY; liveEdgeInPeriodTimeUs = C.TIME_UNSET; mediaDataSource = dataSourceFactory.createDataSource(C.DATA_TYPE_MEDIA); if (mediaTransferListener != null) { @@ -227,10 +223,17 @@ import java.util.Map; * media in previous periods still to be played. * @param loadPositionUs The current load position relative to the period start in microseconds. * @param queue The queue of buffered {@link HlsMediaChunk}s. + * @param allowEndOfStream Whether {@link HlsChunkHolder#endOfStream} is allowed to be set for + * non-empty media playlists. If {@code false}, the last available chunk is returned instead. + * If the media playlist is empty, {@link HlsChunkHolder#endOfStream} is always set. * @param out A holder to populate. */ public void getNextChunk( - long playbackPositionUs, long loadPositionUs, List queue, HlsChunkHolder out) { + long playbackPositionUs, + long loadPositionUs, + List queue, + boolean allowEndOfStream, + HlsChunkHolder out) { HlsMediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1); int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); long bufferedDurationUs = loadPositionUs - playbackPositionUs; @@ -266,6 +269,8 @@ import java.util.Map; } HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); + // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null. + Assertions.checkNotNull(mediaPlaylist); independentSegments = mediaPlaylist.hasIndependentSegments; updateLiveEdgeTimeUs(mediaPlaylist); @@ -281,8 +286,11 @@ import java.util.Map; // behind the live window. selectedTrackIndex = oldTrackIndex; selectedPlaylistUrl = playlistUrls[selectedTrackIndex]; - mediaPlaylist = - playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); + mediaPlaylist = + playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); + // playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be + // non-null. + Assertions.checkNotNull(mediaPlaylist); startOfPlaylistInPeriodUs = mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); chunkMediaSequence = previous.getNextChunkIndex(); @@ -294,15 +302,20 @@ import java.util.Map; } int segmentIndexInPlaylist = (int) (chunkMediaSequence - mediaPlaylist.mediaSequence); - if (segmentIndexInPlaylist >= mediaPlaylist.segments.size()) { + int availableSegmentCount = mediaPlaylist.segments.size(); + if (segmentIndexInPlaylist >= availableSegmentCount) { if (mediaPlaylist.hasEndTag) { - out.endOfStream = true; + if (allowEndOfStream || availableSegmentCount == 0) { + out.endOfStream = true; + return; + } + segmentIndexInPlaylist = availableSegmentCount - 1; } else /* Live */ { out.playlistUrl = selectedPlaylistUrl; seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl); expectedPlaylistUrl = selectedPlaylistUrl; + return; } - return; } // We have a valid playlist snapshot, we can discard any playlist errors at this point. seenExpectedPlaylistError = false; @@ -352,7 +365,8 @@ import java.util.Map; if (chunk instanceof EncryptionKeyChunk) { EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; scratchSpace = encryptionKeyChunk.getDataHolder(); - keyCache.put(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.getResult()); + keyCache.put( + encryptionKeyChunk.dataSpec.uri, Assertions.checkNotNull(encryptionKeyChunk.getResult())); } } @@ -418,6 +432,8 @@ import java.util.Map; } HlsMediaPlaylist playlist = playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */ false); + // Playlist snapshot is valid (checked by if() above) so playlist must be non-null. + Assertions.checkNotNull(playlist); long startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); boolean switchingTrack = trackIndex != oldTrackIndex; @@ -495,11 +511,13 @@ import java.util.Map; if (keyUri == null) { return null; } - if (keyCache.containsKey(keyUri)) { - // The key is present in the key cache. We re-insert it to prevent it from being evicted by + + byte[] encryptionKey = keyCache.remove(keyUri); + if (encryptionKey != null) { + // The key was present in the key cache. We re-insert it to prevent it from being evicted by // the following key addition. Note that removal of the key is necessary to affect the // eviction order. - keyCache.put(keyUri, keyCache.remove(keyUri)); + keyCache.put(keyUri, encryptionKey); return null; } DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); @@ -567,6 +585,7 @@ import java.util.Map; } @Override + @Nullable public Object getSelectionData() { return null; } @@ -575,14 +594,14 @@ import java.util.Map; private static final class EncryptionKeyChunk extends DataChunk { - private byte[] result; + private byte @MonotonicNonNull [] result; public EncryptionKeyChunk( DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, byte[] scratchSpace) { super(dataSource, dataSpec, C.DATA_TYPE_DRM, trackFormat, trackSelectionReason, trackSelectionData, scratchSpace); @@ -593,6 +612,8 @@ import java.util.Map; result = Arrays.copyOf(data, limit); } + /** Return the result of this chunk, or null if loading is not complete. */ + @Nullable public byte[] getResult() { return result; } @@ -644,35 +665,4 @@ import java.util.Map; return segmentStartTimeInPeriodUs + segment.durationUs; } } - - /** - * LRU cache that holds up to {@link #KEY_CACHE_SIZE} full-segment-encryption keys. Which each - * addition, once the cache's size exceeds {@link #KEY_CACHE_SIZE}, the oldest item (according to - * insertion order) is removed. - */ - private static final class FullSegmentEncryptionKeyCache extends LinkedHashMap { - - public FullSegmentEncryptionKeyCache() { - super( - /* initialCapacity= */ KEY_CACHE_SIZE * 2, /* loadFactor= */ 1, /* accessOrder= */ false); - } - - @Override - public byte[] get(Object keyUri) { - if (keyUri == null) { - return null; - } - return super.get(keyUri); - } - - @Override - public byte[] put(Uri keyUri, byte[] key) { - return super.put(keyUri, Assertions.checkNotNull(key)); - } - - @Override - protected boolean removeEldestEntry(Map.Entry entry) { - return size() > KEY_CACHE_SIZE; - } - } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java index 103d89188..ace04145e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.PositionHolder; @@ -70,7 +70,6 @@ public interface HlsExtractorFactory { * @param format A {@link Format} associated with the chunk to extract. * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * information is available in the master playlist. - * @param drmInitData {@link DrmInitData} associated with the chunk. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param responseHeaders The HTTP response headers associated with the media segment or * initialization section to extract. @@ -82,11 +81,10 @@ public interface HlsExtractorFactory { * @throws IOException If an I/O error is encountered while sniffing. */ Result createExtractor( - Extractor previousExtractor, + @Nullable Extractor previousExtractor, Uri uri, Format format, - List muxedCaptionFormats, - DrmInitData drmInitData, + @Nullable List muxedCaptionFormats, TimestampAdjuster timestampAdjuster, Map> responseHeaders, ExtractorInput sniffingExtractorInput) diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 26c1a2219..f9707a87f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; @@ -30,6 +31,7 @@ import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.UriUtil; @@ -39,6 +41,9 @@ import java.io.IOException; import java.math.BigInteger; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * An HLS {@link MediaChunk}. @@ -93,7 +98,9 @@ import java.util.concurrent.atomic.AtomicInteger; /* key= */ null); boolean mediaSegmentEncrypted = mediaSegmentKey != null; byte[] mediaSegmentIv = - mediaSegmentEncrypted ? getEncryptionIvArray(mediaSegment.encryptionIV) : null; + mediaSegmentEncrypted + ? getEncryptionIvArray(Assertions.checkNotNull(mediaSegment.encryptionIV)) + : null; DataSource mediaDataSource = buildDataSource(dataSource, mediaSegmentKey, mediaSegmentIv); // Init segment. @@ -104,7 +111,9 @@ import java.util.concurrent.atomic.AtomicInteger; if (initSegment != null) { initSegmentEncrypted = initSegmentKey != null; byte[] initSegmentIv = - initSegmentEncrypted ? getEncryptionIvArray(initSegment.encryptionIV) : null; + initSegmentEncrypted + ? getEncryptionIvArray(Assertions.checkNotNull(initSegment.encryptionIV)) + : null; Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url); initDataSpec = new DataSpec( @@ -170,6 +179,7 @@ import java.util.concurrent.atomic.AtomicInteger; public static final String PRIV_TIMESTAMP_FRAME_OWNER = "com.apple.streaming.transportStreamTimestamp"; + private static final PositionHolder DUMMY_POSITION_HOLDER = new PositionHolder(); private static final AtomicInteger uidSource = new AtomicInteger(); @@ -188,6 +198,8 @@ import java.util.concurrent.atomic.AtomicInteger; @Nullable private final DataSource initDataSource; @Nullable private final DataSpec initDataSpec; + @Nullable private final Extractor previousExtractor; + private final boolean isMasterTimestampSource; private final boolean hasGapTag; private final TimestampAdjuster timestampAdjuster; @@ -195,15 +207,14 @@ import java.util.concurrent.atomic.AtomicInteger; private final HlsExtractorFactory extractorFactory; @Nullable private final List muxedCaptionFormats; @Nullable private final DrmInitData drmInitData; - @Nullable private final Extractor previousExtractor; private final Id3Decoder id3Decoder; private final ParsableByteArray scratchId3Data; private final boolean mediaSegmentEncrypted; private final boolean initSegmentEncrypted; - private Extractor extractor; + @MonotonicNonNull private Extractor extractor; private boolean isExtractorReusable; - private HlsSampleStreamWrapper output; + @MonotonicNonNull private HlsSampleStreamWrapper output; // nextLoadPosition refers to the init segment if initDataLoadRequired is true. // Otherwise, nextLoadPosition refers to the media segment. private int nextLoadPosition; @@ -217,13 +228,13 @@ import java.util.concurrent.atomic.AtomicInteger; DataSpec dataSpec, Format format, boolean mediaSegmentEncrypted, - DataSource initDataSource, + @Nullable DataSource initDataSource, @Nullable DataSpec initDataSpec, boolean initSegmentEncrypted, Uri playlistUrl, @Nullable List muxedCaptionFormats, int trackSelectionReason, - Object trackSelectionData, + @Nullable Object trackSelectionData, long startTimeUs, long endTimeUs, long chunkMediaSequence, @@ -247,8 +258,9 @@ import java.util.concurrent.atomic.AtomicInteger; chunkMediaSequence); this.mediaSegmentEncrypted = mediaSegmentEncrypted; this.discontinuitySequenceNumber = discontinuitySequenceNumber; - this.initDataSource = initDataSource; this.initDataSpec = initDataSpec; + this.initDataSource = initDataSource; + this.initDataLoadRequired = initDataSpec != null; this.initSegmentEncrypted = initSegmentEncrypted; this.playlistUrl = playlistUrl; this.isMasterTimestampSource = isMasterTimestampSource; @@ -261,7 +273,6 @@ import java.util.concurrent.atomic.AtomicInteger; this.id3Decoder = id3Decoder; this.scratchId3Data = scratchId3Data; this.shouldSpliceIn = shouldSpliceIn; - initDataLoadRequired = initDataSpec != null; uid = uidSource.getAndIncrement(); } @@ -273,6 +284,7 @@ import java.util.concurrent.atomic.AtomicInteger; */ public void init(HlsSampleStreamWrapper output) { this.output = output; + output.init(uid, shouldSpliceIn); } @Override @@ -289,11 +301,12 @@ import java.util.concurrent.atomic.AtomicInteger; @Override public void load() throws IOException, InterruptedException { + // output == null means init() hasn't been called. + Assertions.checkNotNull(output); if (extractor == null && previousExtractor != null) { extractor = previousExtractor; isExtractorReusable = true; initDataLoadRequired = false; - output.init(uid, shouldSpliceIn, /* reusingExtractor= */ true); } maybeLoadInitData(); if (!loadCanceled) { @@ -306,15 +319,20 @@ import java.util.concurrent.atomic.AtomicInteger; // Internal methods. + @RequiresNonNull("output") private void maybeLoadInitData() throws IOException, InterruptedException { if (!initDataLoadRequired) { return; } + // initDataLoadRequired => initDataSource != null && initDataSpec != null + Assertions.checkNotNull(initDataSource); + Assertions.checkNotNull(initDataSpec); feedDataToExtractor(initDataSource, initDataSpec, initSegmentEncrypted); nextLoadPosition = 0; initDataLoadRequired = false; } + @RequiresNonNull("output") private void loadMedia() throws IOException, InterruptedException { if (!isMasterTimestampSource) { timestampAdjuster.waitUntilInitialized(); @@ -330,6 +348,7 @@ import java.util.concurrent.atomic.AtomicInteger; * concludes (because of a thrown exception or because the operation finishes), the number of fed * bytes is written to {@code nextLoadPosition}. */ + @RequiresNonNull("output") private void feedDataToExtractor( DataSource dataSource, DataSpec dataSpec, boolean dataIsEncrypted) throws IOException, InterruptedException { @@ -354,7 +373,7 @@ import java.util.concurrent.atomic.AtomicInteger; try { int result = Extractor.RESULT_CONTINUE; while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - result = extractor.read(input, /* seekPosition= */ null); + result = extractor.read(input, DUMMY_POSITION_HOLDER); } } finally { nextLoadPosition = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); @@ -364,10 +383,11 @@ import java.util.concurrent.atomic.AtomicInteger; } } + @RequiresNonNull("output") + @EnsuresNonNull("extractor") private DefaultExtractorInput prepareExtraction(DataSource dataSource, DataSpec dataSpec) throws IOException, InterruptedException { long bytesToRead = dataSource.open(dataSpec); - DefaultExtractorInput extractorInput = new DefaultExtractorInput(dataSource, dataSpec.absoluteStreamPosition, bytesToRead); @@ -381,7 +401,6 @@ import java.util.concurrent.atomic.AtomicInteger; dataSpec.uri, trackFormat, muxedCaptionFormats, - drmInitData, timestampAdjuster, dataSource.getResponseHeaders(), extractorInput); @@ -397,10 +416,10 @@ import java.util.concurrent.atomic.AtomicInteger; // the timestamp offset. output.setSampleOffsetUs(/* sampleOffsetUs= */ 0L); } - output.init(uid, shouldSpliceIn, /* reusingExtractor= */ false); + output.onNewExtractor(); extractor.init(output); } - + output.setDrmInitData(drmInitData); return extractorInput; } @@ -483,10 +502,15 @@ import java.util.concurrent.atomic.AtomicInteger; /** * If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original * in order to decrypt the loaded data. Else returns the original. + * + *

    {@code fullSegmentEncryptionKey} & {@code encryptionIv} can either both be null, or neither. */ - private static DataSource buildDataSource(DataSource dataSource, byte[] fullSegmentEncryptionKey, - byte[] encryptionIv) { + private static DataSource buildDataSource( + DataSource dataSource, + @Nullable byte[] fullSegmentEncryptionKey, + @Nullable byte[] encryptionIv) { if (fullSegmentEncryptionKey != null) { + Assertions.checkNotNull(encryptionIv); return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv); } return dataSource; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 287dfe137..3b723af43 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -16,12 +16,14 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.offline.StreamKey; @@ -46,13 +48,14 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link MediaPeriod} that loads an HLS stream. @@ -63,7 +66,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final HlsExtractorFactory extractorFactory; private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; - private final @Nullable TransferListener mediaTransferListener; + @Nullable private final TransferListener mediaTransferListener; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; @@ -71,12 +75,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final TimestampAdjusterProvider timestampAdjusterProvider; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final boolean allowChunklessPreparation; - private final @HlsMetadataType int metadataType; + private final @HlsMediaSource.MetadataType int metadataType; private final boolean useSessionKeys; - private @Nullable Callback callback; + @Nullable private Callback callback; private int pendingPrepareCount; - private TrackGroupArray trackGroups; + private @MonotonicNonNull TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; // Maps sample stream wrappers to variant/rendition index by matching array positions. @@ -93,6 +97,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper * and keys. * @param mediaTransferListener The transfer listener to inform of any media data transfers. May * be null if no listener is available. + * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession + * DrmSessions} with. * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. @@ -106,17 +112,19 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, @Nullable TransferListener mediaTransferListener, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, Allocator allocator, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, boolean allowChunklessPreparation, - @HlsMetadataType int metadataType, + @HlsMediaSource.MetadataType int metadataType, boolean useSessionKeys) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.mediaTransferListener = mediaTransferListener; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; @@ -159,7 +167,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public TrackGroupArray getTrackGroups() { - return trackGroups; + // trackGroups will only be null if period hasn't been prepared or has been released. + return Assertions.checkNotNull(trackGroups); } // TODO: When the master playlist does not de-duplicate variants by URL and allows Renditions with @@ -246,8 +255,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { // Map each selection and stream onto a child period index. int[] streamChildIndices = new int[selections.length]; int[] selectionChildIndices = new int[selections.length]; @@ -270,8 +283,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper streamWrapperIndices.clear(); // Select tracks for each child, copying the resulting streams back into a new streams array. SampleStream[] newStreams = new SampleStream[selections.length]; - SampleStream[] childStreams = new SampleStream[selections.length]; - TrackSelection[] childSelections = new TrackSelection[selections.length]; + @NullableType SampleStream[] childStreams = new SampleStream[selections.length]; + @NullableType TrackSelection[] childSelections = new TrackSelection[selections.length]; int newEnabledSampleStreamWrapperCount = 0; HlsSampleStreamWrapper[] newEnabledSampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrappers.length]; @@ -285,15 +298,16 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper childStreams, streamResetFlags, positionUs, forceReset); boolean wrapperEnabled = false; for (int j = 0; j < selections.length; j++) { + SampleStream childStream = childStreams[j]; if (selectionChildIndices[j] == i) { // Assert that the child provided a stream for the selection. - Assertions.checkState(childStreams[j] != null); - newStreams[j] = childStreams[j]; + Assertions.checkNotNull(childStream); + newStreams[j] = childStream; wrapperEnabled = true; - streamWrapperIndices.put(childStreams[j], i); + streamWrapperIndices.put(childStream, i); } else if (streamChildIndices[j] == i) { // Assert that the child cleared any previous stream. - Assertions.checkState(childStreams[j] == null); + Assertions.checkState(childStream == null); } } if (wrapperEnabled) { @@ -317,8 +331,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Copy the new streams back into the streams array. System.arraycopy(newStreams, 0, streams, 0, newStreams.length); // Update the local state. - enabledSampleStreamWrappers = Arrays.copyOf(newEnabledSampleStreamWrappers, - newEnabledSampleStreamWrapperCount); + enabledSampleStreamWrappers = + Util.nullSafeArrayCopy(newEnabledSampleStreamWrappers, newEnabledSampleStreamWrapperCount); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader( enabledSampleStreamWrappers); @@ -350,6 +364,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); + } + @Override public long getNextLoadPositionUs() { return compositeSequenceableLoader.getNextLoadPositionUs(); @@ -491,7 +510,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper manifestUrlIndicesPerWrapper.add(new int[] {i}); sampleStreamWrappers.add(sampleStreamWrapper); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(subtitleRendition.format)), 0, TrackGroupArray.EMPTY); + new TrackGroup[] {new TrackGroup(subtitleRendition.format)}, + /* primaryTrackGroupIndex= */ 0); } this.sampleStreamWrappers = sampleStreamWrappers.toArray(new HlsSampleStreamWrapper[0]); @@ -649,9 +669,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper muxedTrackGroups.add(id3TrackGroup); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), - 0, - new TrackGroupArray(id3TrackGroup)); + muxedTrackGroups.toArray(new TrackGroup[0]), + /* primaryTrackGroupIndex= */ 0, + /* optionalTrackGroupsIndices= */ muxedTrackGroups.indexOf(id3TrackGroup)); } } @@ -695,7 +715,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper( C.TRACK_TYPE_AUDIO, - scratchPlaylistUrls.toArray(new Uri[0]), + scratchPlaylistUrls.toArray(Util.castNonNullTypeArray(new Uri[0])), scratchPlaylistFormats.toArray(new Format[0]), /* muxedAudioFormat= */ null, /* muxedCaptionFormats= */ Collections.emptyList(), @@ -707,7 +727,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (allowChunklessPreparation && renditionsHaveCodecs) { Format[] renditionFormats = scratchPlaylistFormats.toArray(new Format[0]); sampleStreamWrapper.prepareWithMasterPlaylistInfo( - new TrackGroupArray(new TrackGroup(renditionFormats)), 0, TrackGroupArray.EMPTY); + new TrackGroup[] {new TrackGroup(renditionFormats)}, /* primaryTrackGroupIndex= */ 0); } } } @@ -716,8 +736,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper int trackType, Uri[] playlistUrls, Format[] playlistFormats, - Format muxedAudioFormat, - List muxedCaptionFormats, + @Nullable Format muxedAudioFormat, + @Nullable List muxedCaptionFormats, Map overridingDrmInitData, long positionUs) { HlsChunkSource defaultChunkSource = @@ -738,6 +758,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper allocator, positionUs, muxedAudioFormat, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, metadataType); @@ -789,7 +810,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } private static Format deriveAudioFormat( - Format variantFormat, Format mediaTagFormat, boolean isPrimaryTrackInVariant) { + Format variantFormat, @Nullable Format mediaTagFormat, boolean isPrimaryTrackInVariant) { String codecs; Metadata metadata; int channelCount = Format.NO_VALUE; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index f75b52e35..cc2fe618f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -15,11 +15,16 @@ */ package com.google.android.exoplayer2.source.hls; +import static java.lang.annotation.RetentionPolicy.SOURCE; + import android.net.Uri; import android.os.Handler; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -29,9 +34,9 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistParserFactory; import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; import com.google.android.exoplayer2.source.hls.playlist.FilteringHlsPlaylistParserFactory; @@ -45,6 +50,8 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; import java.util.List; /** An HLS {@link MediaSource}. */ @@ -55,8 +62,30 @@ public final class HlsMediaSource extends BaseMediaSource ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } + /** + * The types of metadata that can be extracted from HLS streams. + * + *

    Allowed values: + * + *

      + *
    • {@link #METADATA_TYPE_ID3} + *
    • {@link #METADATA_TYPE_EMSG} + *
    + * + *

    See {@link Factory#setMetadataType(int)}. + */ + @Documented + @Retention(SOURCE) + @IntDef({METADATA_TYPE_ID3, METADATA_TYPE_EMSG}) + public @interface MetadataType {} + + /** Type for ID3 metadata in HLS streams. */ + public static final int METADATA_TYPE_ID3 = 1; + /** Type for ESMG metadata in HLS streams. */ + public static final int METADATA_TYPE_EMSG = 3; + /** Factory for {@link HlsMediaSource}s. */ - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final HlsDataSourceFactory hlsDataSourceFactory; @@ -65,9 +94,10 @@ public final class HlsMediaSource extends BaseMediaSource @Nullable private List streamKeys; private HlsPlaylistTracker.Factory playlistTrackerFactory; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private boolean allowChunklessPreparation; - @HlsMetadataType private int metadataType; + @MetadataType private int metadataType; private boolean useSessionKeys; private boolean isCreateCalled; @Nullable private Object tag; @@ -94,9 +124,10 @@ public final class HlsMediaSource extends BaseMediaSource playlistParserFactory = new DefaultHlsPlaylistParserFactory(); playlistTrackerFactory = DefaultHlsPlaylistTracker.FACTORY; extractorFactory = HlsExtractorFactory.DEFAULT; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - metadataType = HlsMetadataType.ID3; + metadataType = METADATA_TYPE_ID3; } /** @@ -108,7 +139,7 @@ public final class HlsMediaSource extends BaseMediaSource * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; @@ -179,19 +210,6 @@ public final class HlsMediaSource extends BaseMediaSource return this; } - /** - * Sets a list of {@link StreamKey stream keys} by which the playlists are filtered. - * - * @param streamKeys A list of {@link StreamKey stream keys}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; - } - /** * Sets the {@link HlsPlaylistTracker} factory. The default value is {@link * DefaultHlsPlaylistTracker#FACTORY}. @@ -241,24 +259,24 @@ public final class HlsMediaSource extends BaseMediaSource /** * Sets the type of metadata to extract from the HLS source (defaults to {@link - * HlsMetadataType#ID3}). + * #METADATA_TYPE_ID3}). * *

    HLS supports in-band ID3 in both TS and fMP4 streams, but in the fMP4 case the data is * wrapped in an EMSG box [spec]. * - *

    If this is set to {@link HlsMetadataType#ID3} then raw ID3 metadata of will be extracted + *

    If this is set to {@link #METADATA_TYPE_ID3} then raw ID3 metadata of will be extracted * from TS sources. From fMP4 streams EMSGs containing metadata of this type (in the variant * stream only) will be unwrapped to expose the inner data. All other in-band metadata will be * dropped. * - *

    If this is set to {@link HlsMetadataType#EMSG} then all EMSG data from the fMP4 variant + *

    If this is set to {@link #METADATA_TYPE_EMSG} then all EMSG data from the fMP4 variant * stream will be extracted. No metadata will be extracted from TS streams, since they don't * support EMSG. * * @param metadataType The type of metadata to extract. * @return This factory, for convenience. */ - public Factory setMetadataType(@HlsMetadataType int metadataType) { + public Factory setMetadataType(@MetadataType int metadataType) { Assertions.checkState(!isCreateCalled); this.metadataType = metadataType; return this; @@ -278,6 +296,40 @@ public final class HlsMediaSource extends BaseMediaSource return this; } + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public HlsMediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + HlsMediaSource mediaSource = createMediaSource(playlistUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + @Override + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); + return this; + } + /** * Returns a new {@link HlsMediaSource} using the current parameters. * @@ -295,6 +347,7 @@ public final class HlsMediaSource extends BaseMediaSource hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, playlistTrackerFactory.createTracker( hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory), @@ -304,20 +357,11 @@ public final class HlsMediaSource extends BaseMediaSource tag); } - /** - * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, - * MediaSourceEventListener)} instead. - */ - @Deprecated - public HlsMediaSource createMediaSource( - Uri playlistUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { - HlsMediaSource mediaSource = createMediaSource(playlistUri); - if (eventHandler != null && eventListener != null) { - mediaSource.addEventListener(eventHandler, eventListener); - } - return mediaSource; + @Override + public Factory setStreamKeys(List streamKeys) { + Assertions.checkState(!isCreateCalled); + this.streamKeys = streamKeys; + return this; } @Override @@ -331,30 +375,33 @@ public final class HlsMediaSource extends BaseMediaSource private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean allowChunklessPreparation; - private final @HlsMetadataType int metadataType; + private final @MetadataType int metadataType; private final boolean useSessionKeys; private final HlsPlaylistTracker playlistTracker; - private final @Nullable Object tag; + @Nullable private final Object tag; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private HlsMediaSource( Uri manifestUri, HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, - @HlsMetadataType int metadataType, + @MetadataType int metadataType, boolean useSessionKeys, @Nullable Object tag) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.playlistTracker = playlistTracker; this.allowChunklessPreparation = allowChunklessPreparation; @@ -370,8 +417,9 @@ public final class HlsMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; + drmSessionManager.prepare(); EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this); } @@ -389,6 +437,7 @@ public final class HlsMediaSource extends BaseMediaSource playlistTracker, dataSourceFactory, mediaTransferListener, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, allocator, @@ -404,8 +453,9 @@ public final class HlsMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { playlistTracker.stop(); + drmSessionManager.release(); } @Override @@ -421,6 +471,9 @@ public final class HlsMediaSource extends BaseMediaSource ? windowStartTimeMs : C.TIME_UNSET; long windowDefaultStartPositionUs = playlist.startOffsetUs; + // masterPlaylist is non-null because the first playlist has been fetched by now. + HlsManifest manifest = + new HlsManifest(Assertions.checkNotNull(playlistTracker.getMasterPlaylist()), playlist); if (playlistTracker.isLive()) { long offsetFromInitialStartTimeUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); @@ -428,8 +481,18 @@ public final class HlsMediaSource extends BaseMediaSource playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET; List segments = playlist.segments; if (windowDefaultStartPositionUs == C.TIME_UNSET) { - windowDefaultStartPositionUs = segments.isEmpty() ? 0 - : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs; + windowDefaultStartPositionUs = 0; + if (!segments.isEmpty()) { + int defaultStartSegmentIndex = Math.max(0, segments.size() - 3); + // We attempt to set the default start position to be at least twice the target duration + // behind the live edge. + long minStartPositionUs = playlist.durationUs - playlist.targetDurationUs * 2; + while (defaultStartSegmentIndex > 0 + && segments.get(defaultStartSegmentIndex).relativeStartTimeUs > minStartPositionUs) { + defaultStartSegmentIndex--; + } + windowDefaultStartPositionUs = segments.get(defaultStartSegmentIndex).relativeStartTimeUs; + } } timeline = new SinglePeriodTimeline( @@ -441,6 +504,8 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ !playlist.hasEndTag, + /* isLive= */ true, + manifest, tag); } else /* not live */ { if (windowDefaultStartPositionUs == C.TIME_UNSET) { @@ -456,9 +521,11 @@ public final class HlsMediaSource extends BaseMediaSource windowDefaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ false, + /* isLive= */ false, + manifest, tag); } - refreshSourceInfo(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); + refreshSourceInfo(timeline); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index cf879e91c..c820038b8 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -62,8 +62,11 @@ import java.io.IOException; if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); + } else if (sampleQueueIndex == HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_PENDING) { + sampleStreamWrapper.maybeThrowError(); + } else if (sampleQueueIndex != HlsSampleStreamWrapper.SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL) { + sampleStreamWrapper.maybeThrowError(sampleQueueIndex); } - sampleStreamWrapper.maybeThrowError(); } @Override diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 67abb7bf0..c7116ba87 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -17,14 +17,19 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.util.SparseIntArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DummyTrackOutput; +import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; @@ -61,7 +66,10 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.checkerframework.checker.nullness.compatqual.NullableType; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides @@ -105,40 +113,42 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final Callback callback; private final HlsChunkSource chunkSource; private final Allocator allocator; - private final Format muxedAudioFormat; + @Nullable private final Format muxedAudioFormat; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final Loader loader; private final EventDispatcher eventDispatcher; - private final @HlsMetadataType int metadataType; + private final @HlsMediaSource.MetadataType int metadataType; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final ArrayList mediaChunks; private final List readOnlyMediaChunks; + // Using runnables rather than in-line method references to avoid repeated allocations. private final Runnable maybeFinishPrepareRunnable; private final Runnable onTracksEndedRunnable; private final Handler handler; private final ArrayList hlsSampleStreams; private final Map overridingDrmInitData; - private SampleQueue[] sampleQueues; + private FormatAdjustingSampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; private Set sampleQueueMappingDoneByType; private SparseIntArray sampleQueueIndicesByType; - private TrackOutput emsgUnwrappingTrackOutput; + @MonotonicNonNull private TrackOutput emsgUnwrappingTrackOutput; private int primarySampleQueueType; private int primarySampleQueueIndex; private boolean sampleQueuesBuilt; private boolean prepared; private int enabledTrackGroupCount; - private Format upstreamTrackFormat; - private Format downstreamTrackFormat; + @MonotonicNonNull private Format upstreamTrackFormat; + @Nullable private Format downstreamTrackFormat; private boolean released; // Tracks are complicated in HLS. See documentation of buildTracksFromSampleStreams for details. // Indexed by track (as exposed by this source). - private TrackGroupArray trackGroups; - private TrackGroupArray optionalTrackGroups; + @MonotonicNonNull private TrackGroupArray trackGroups; + @MonotonicNonNull private Set optionalTrackGroups; // Indexed by track group. - private int[] trackGroupToSampleQueueIndex; + private int @MonotonicNonNull [] trackGroupToSampleQueueIndex; private int primaryTrackGroupIndex; private boolean haveAudioVideoSampleQueues; private boolean[] sampleQueuesEnabledStates; @@ -153,6 +163,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Accessed only by the loading thread. private boolean tracksEnded; private long sampleOffsetUs; + @Nullable private DrmInitData drmInitData; private int chunkUid; /** @@ -166,6 +177,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param positionUs The position from which to start loading media. * @param muxedAudioFormat Optional muxed audio {@link Format} as defined by the master playlist. + * @param drmSessionManager The {@link DrmSessionManager} to acquire {@link DrmSession + * DrmSessions} with. * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. * @param eventDispatcher A dispatcher to notify of events. */ @@ -176,16 +189,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Map overridingDrmInitData, Allocator allocator, long positionUs, - Format muxedAudioFormat, + @Nullable Format muxedAudioFormat, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, - @HlsMetadataType int metadataType) { + @HlsMediaSource.MetadataType int metadataType) { this.trackType = trackType; this.callback = callback; this.chunkSource = chunkSource; this.overridingDrmInitData = overridingDrmInitData; this.allocator = allocator; this.muxedAudioFormat = muxedAudioFormat; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.metadataType = metadataType; @@ -194,14 +209,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; sampleQueueTrackIds = new int[0]; sampleQueueMappingDoneByType = new HashSet<>(MAPPABLE_TYPES.size()); sampleQueueIndicesByType = new SparseIntArray(MAPPABLE_TYPES.size()); - sampleQueues = new SampleQueue[0]; + sampleQueues = new FormatAdjustingSampleQueue[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); hlsSampleStreams = new ArrayList<>(); - maybeFinishPrepareRunnable = this::maybeFinishPrepare; - onTracksEndedRunnable = this::onTracksEnded; + // Suppressions are needed because `this` is not initialized here. + @SuppressWarnings("nullness:methodref.receiver.bound.invalid") + Runnable maybeFinishPrepareRunnable = this::maybeFinishPrepare; + this.maybeFinishPrepareRunnable = maybeFinishPrepareRunnable; + @SuppressWarnings("nullness:methodref.receiver.bound.invalid") + Runnable onTracksEndedRunnable = this::onTracksEnded; + this.onTracksEndedRunnable = onTracksEndedRunnable; handler = new Handler(); lastSeekPositionUs = positionUs; pendingResetPositionUs = positionUs; @@ -216,27 +236,33 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Prepares the sample stream wrapper with master playlist information. * - * @param trackGroups The {@link TrackGroupArray} to expose. + * @param trackGroups The {@link TrackGroup TrackGroups} to expose through {@link + * #getTrackGroups()}. * @param primaryTrackGroupIndex The index of the adaptive track group. - * @param optionalTrackGroups A subset of {@code trackGroups} that should not trigger a failure if - * not found in the media playlist's segments. + * @param optionalTrackGroupsIndices The indices of any {@code trackGroups} that should not + * trigger a failure if not found in the media playlist's segments. */ public void prepareWithMasterPlaylistInfo( - TrackGroupArray trackGroups, - int primaryTrackGroupIndex, - TrackGroupArray optionalTrackGroups) { - prepared = true; - this.trackGroups = trackGroups; - this.optionalTrackGroups = optionalTrackGroups; + TrackGroup[] trackGroups, int primaryTrackGroupIndex, int... optionalTrackGroupsIndices) { + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); + optionalTrackGroups = new HashSet<>(); + for (int optionalTrackGroupIndex : optionalTrackGroupsIndices) { + optionalTrackGroups.add(this.trackGroups.get(optionalTrackGroupIndex)); + } this.primaryTrackGroupIndex = primaryTrackGroupIndex; handler.post(callback::onPrepared); + setIsPrepared(); } public void maybeThrowPrepareError() throws IOException { maybeThrowError(); + if (loadingFinished && !prepared) { + throw new ParserException("Loading finished before preparation is complete."); + } } public TrackGroupArray getTrackGroups() { + assertIsPrepared(); return trackGroups; } @@ -245,11 +271,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } public int bindSampleQueueToSampleStream(int trackGroupIndex) { + assertIsPrepared(); + Assertions.checkNotNull(trackGroupToSampleQueueIndex); + int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { - return optionalTrackGroups.indexOf(trackGroups.get(trackGroupIndex)) == C.INDEX_UNSET - ? SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL - : SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL; + return optionalTrackGroups.contains(trackGroups.get(trackGroupIndex)) + ? SAMPLE_QUEUE_INDEX_NO_MAPPING_NON_FATAL + : SAMPLE_QUEUE_INDEX_NO_MAPPING_FATAL; } if (sampleQueuesEnabledStates[sampleQueueIndex]) { // This sample queue is already bound to a different sample stream. @@ -260,6 +289,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } public void unbindSampleQueue(int trackGroupIndex) { + assertIsPrepared(); + Assertions.checkNotNull(trackGroupToSampleQueueIndex); int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex]); sampleQueuesEnabledStates[sampleQueueIndex] = false; @@ -282,15 +313,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @return Whether this wrapper requires the parent {@link HlsMediaPeriod} to perform a seek as * part of the track selection. */ - public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) { - Assertions.checkState(prepared); + public boolean selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs, + boolean forceReset) { + assertIsPrepared(); int oldEnabledTrackGroupCount = enabledTrackGroupCount; // Deselect old tracks. for (int i = 0; i < selections.length; i++) { - if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + HlsSampleStream stream = (HlsSampleStream) streams[i]; + if (stream != null && (selections[i] == null || !mayRetainStreamFlags[i])) { enabledTrackGroupCount--; - ((HlsSampleStream) streams[i]).unbindSampleQueue(); + stream.unbindSampleQueue(); streams[i] = null; } } @@ -323,17 +360,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; streamResetFlags[i] = true; if (trackGroupToSampleQueueIndex != null) { ((HlsSampleStream) streams[i]).bindSampleQueue(); - } - // If there's still a chance of avoiding a seek, try and seek within the sample queue. - if (sampleQueuesBuilt && !seekRequired) { - SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; - sampleQueue.rewind(); - // A seek can be avoided if we're able to advance to the current playback position in the - // sample queue, or if we haven't read anything from the queue since the previous seek - // (this case is common for sparse tracks such as metadata tracks). In all other cases a - // seek is required. - seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED - && sampleQueue.getReadIndex() != 0; + // If there's still a chance of avoiding a seek, try and seek within the sample queue. + if (!seekRequired) { + SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; + // A seek can be avoided if we're able to seek to the current playback position in + // the sample queue, or if we haven't read anything from the queue since the previous + // seek (this case is common for sparse tracks such as metadata tracks). In all other + // cases a seek is required. + seekRequired = + !sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ true) + && sampleQueue.getReadIndex() != 0; + } } } } @@ -452,7 +489,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise // sampleQueues may still be being modified by the loading thread. for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.discardToEnd(); + sampleQueue.preRelease(); } } loader.release(this); @@ -463,7 +500,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void onLoaderReleased() { - resetSampleQueues(); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.release(); + } } public void setIsTimestampMaster(boolean isTimestampMaster) { @@ -477,7 +516,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // SampleStream implementation. public boolean isReady(int sampleQueueIndex) { - return loadingFinished || (!isPendingReset() && sampleQueues[sampleQueueIndex].hasNextSample()); + return !isPendingReset() && sampleQueues[sampleQueueIndex].isReady(loadingFinished); + } + + public void maybeThrowError(int sampleQueueIndex) throws IOException { + maybeThrowError(); + sampleQueues[sampleQueueIndex].maybeThrowError(); } public void maybeThrowError() throws IOException { @@ -513,7 +557,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; sampleQueues[sampleQueueIndex].read( formatHolder, buffer, requireFormat, loadingFinished, lastSeekPositionUs); if (result == C.RESULT_FORMAT_READ) { - Format format = formatHolder.format; + Format format = Assertions.checkNotNull(formatHolder.format); if (sampleQueueIndex == primarySampleQueueIndex) { // Fill in primary sample format with information from the track format. int chunkUid = sampleQueues[sampleQueueIndex].peekSourceId(); @@ -524,15 +568,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Format trackFormat = chunkIndex < mediaChunks.size() ? mediaChunks.get(chunkIndex).trackFormat - : upstreamTrackFormat; + : Assertions.checkNotNull(upstreamTrackFormat); format = format.copyWithManifestFormatInfo(trackFormat); } - if (format.drmInitData != null) { - DrmInitData drmInitData = overridingDrmInitData.get(format.drmInitData.schemeType); - if (drmInitData != null) { - format = format.copyWithDrmInitData(drmInitData); - } - } formatHolder.format = format; } return result; @@ -547,8 +585,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); } else { - int skipCount = sampleQueue.advanceTo(positionUs, true, true); - return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; + return sampleQueue.advanceTo(positionUs); } } @@ -606,7 +643,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ? lastMediaChunk.endTimeUs : Math.max(lastSeekPositionUs, lastMediaChunk.startTimeUs); } - chunkSource.getNextChunk(positionUs, loadPositionUs, chunkQueue, nextChunkHolder); + chunkSource.getNextChunk( + positionUs, + loadPositionUs, + chunkQueue, + /* allowEndOfStream= */ prepared || !chunkQueue.isEmpty(), + nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; Uri playlistUrlToLoad = nextChunkHolder.playlistUrl; @@ -648,6 +690,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return true; } + @Override + public boolean isLoading() { + return loader.isLoading(); + } + @Override public void reevaluateBuffer(long positionUs) { // Do nothing. @@ -777,13 +824,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param chunkUid The chunk's uid. * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any * samples already queued to the wrapper. - * @param reusingExtractor Whether the extractor for the chunk has already been used for preceding - * chunks. */ - public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) { - if (!reusingExtractor) { - sampleQueueMappingDoneByType.clear(); - } + public void init(int chunkUid, boolean shouldSpliceIn) { this.chunkUid = chunkUid; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); @@ -838,8 +880,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * different ID, then return a {@link DummyTrackOutput} that does nothing. * *

    If a {@link SampleQueue} for {@code type} has been created but is not mapped, then map it to - * this {@code id} and return it. This situation can happen after a call to {@link #init} with - * {@code reusingExtractor=false}. + * this {@code id} and return it. This situation can happen after a call to {@link + * #onNewExtractor}. * * @param id The ID of the track. * @param type The type of the track, must be one of {@link #MAPPABLE_TYPES}. @@ -864,17 +906,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private SampleQueue createSampleQueue(int id, int type) { int trackCount = sampleQueues.length; - SampleQueue trackOutput = new PrivTimestampStrippingSampleQueue(allocator); + boolean isAudioVideo = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; + FormatAdjustingSampleQueue trackOutput = + new FormatAdjustingSampleQueue( + allocator, + /* playbackLooper= */ handler.getLooper(), + drmSessionManager, + overridingDrmInitData); + if (isAudioVideo) { + trackOutput.setDrmInitData(drmInitData); + } trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.sourceId(chunkUid); trackOutput.setUpstreamFormatChangeListener(this); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); sampleQueueTrackIds[trackCount] = id; - sampleQueues = Arrays.copyOf(sampleQueues, trackCount + 1); - sampleQueues[trackCount] = trackOutput; + sampleQueues = Util.nullSafeArrayAppend(sampleQueues, trackOutput); sampleQueueIsAudioVideoFlags = Arrays.copyOf(sampleQueueIsAudioVideoFlags, trackCount + 1); - sampleQueueIsAudioVideoFlags[trackCount] = - type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; + sampleQueueIsAudioVideoFlags[trackCount] = isAudioVideo; haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; sampleQueueMappingDoneByType.add(type); sampleQueueIndicesByType.append(type, trackCount); @@ -906,16 +955,64 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Called by the loading thread. + /** Called when an {@link HlsMediaChunk} starts extracting media with a new {@link Extractor}. */ + public void onNewExtractor() { + sampleQueueMappingDoneByType.clear(); + } + + /** + * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples that + * are subsequently loaded by this wrapper. + * + * @param sampleOffsetUs The timestamp offset in microseconds. + */ public void setSampleOffsetUs(long sampleOffsetUs) { - this.sampleOffsetUs = sampleOffsetUs; - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.setSampleOffsetUs(sampleOffsetUs); + if (this.sampleOffsetUs != sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.setSampleOffsetUs(sampleOffsetUs); + } + } + } + + /** + * Sets default {@link DrmInitData} for samples that are subsequently loaded by this wrapper. + * + *

    This method should be called prior to loading each {@link HlsMediaChunk}. The {@link + * DrmInitData} passed should be that of an EXT-X-KEY tag that applies to the chunk, or {@code + * null} otherwise. + * + *

    The final {@link DrmInitData} for subsequently queued samples is determined as followed: + * + *

      + *
    1. It is initially set to {@code drmInitData}, unless {@code drmInitData} is null in which + * case it's set to {@link Format#drmInitData} of the upstream {@link Format}. + *
    2. If the initial {@link DrmInitData} is non-null and {@link #overridingDrmInitData} + * contains an entry whose key matches the {@link DrmInitData#schemeType}, then the sample's + * {@link DrmInitData} is overridden to be this entry's value. + *
    + * + *

    + * + * @param drmInitData The default {@link DrmInitData} for samples that are subsequently queued. If + * non-null then it takes precedence over {@link Format#drmInitData} of the upstream {@link + * Format}, but will still be overridden by a matching override in {@link + * #overridingDrmInitData}. + */ + public void setDrmInitData(@Nullable DrmInitData drmInitData) { + if (!Util.areEqual(this.drmInitData, drmInitData)) { + this.drmInitData = drmInitData; + for (int i = 0; i < sampleQueues.length; i++) { + if (sampleQueueIsAudioVideoFlags[i]) { + sampleQueues[i].setDrmInitData(drmInitData); + } + } } } // Internal methods. - private void updateSampleStreams(SampleStream[] streams) { + private void updateSampleStreams(@NullableType SampleStream[] streams) { hlsSampleStreams.clear(); for (SampleStream stream : streams) { if (stream != null) { @@ -963,11 +1060,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } else { // Tracks are created using media segment information. buildTracksFromSampleStreams(); - prepared = true; + setIsPrepared(); callback.onPrepared(); } } + @RequiresNonNull("trackGroups") + @EnsuresNonNull("trackGroupToSampleQueueIndex") private void mapSampleQueuesToMatchTrackGroups() { int trackGroupCount = trackGroups.length; trackGroupToSampleQueueIndex = new int[trackGroupCount]; @@ -1016,6 +1115,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * unchanged. *

*/ + @EnsuresNonNull({"trackGroups", "optionalTrackGroups", "trackGroupToSampleQueueIndex"}) private void buildTracksFromSampleStreams() { // Iterate through the extractor tracks to discover the "primary" track type, and the index // of the single track of this type. @@ -1079,9 +1179,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false)); } } - this.trackGroups = new TrackGroupArray(trackGroups); + this.trackGroups = createTrackGroupArrayWithDrmInfo(trackGroups); Assertions.checkState(optionalTrackGroups == null); - optionalTrackGroups = TrackGroupArray.EMPTY; + optionalTrackGroups = Collections.emptySet(); + } + + private TrackGroupArray createTrackGroupArrayWithDrmInfo(TrackGroup[] trackGroups) { + for (int i = 0; i < trackGroups.length; i++) { + TrackGroup trackGroup = trackGroups[i]; + Format[] exposedFormats = new Format[trackGroup.length]; + for (int j = 0; j < trackGroup.length; j++) { + Format format = trackGroup.getFormat(j); + if (format.drmInitData != null) { + format = + format.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(format.drmInitData)); + } + exposedFormats[j] = format; + } + trackGroups[i] = new TrackGroup(exposedFormats); + } + return new TrackGroupArray(trackGroups); } private HlsMediaChunk getLastMediaChunk() { @@ -1102,9 +1220,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; int sampleQueueCount = sampleQueues.length; for (int i = 0; i < sampleQueueCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; - sampleQueue.rewind(); - boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) - != SampleQueue.ADVANCE_FAILED; + boolean seekInsideQueue = sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); // If we have AV tracks then an in-queue seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is @@ -1116,6 +1232,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return true; } + @RequiresNonNull({"trackGroups", "optionalTrackGroups"}) + private void setIsPrepared() { + prepared = true; + } + + @EnsuresNonNull({"trackGroups", "optionalTrackGroups"}) + private void assertIsPrepared() { + Assertions.checkState(prepared); + Assertions.checkNotNull(trackGroups); + Assertions.checkNotNull(optionalTrackGroups); + } + /** * Scores a track type. Where multiple tracks are muxed into a container, the track with the * highest score is the primary track. @@ -1147,7 +1275,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @return The derived track format. */ private static Format deriveFormat( - Format playlistFormat, Format sampleFormat, boolean propagateBitrate) { + @Nullable Format playlistFormat, Format sampleFormat, boolean propagateBitrate) { if (playlistFormat == null) { return sampleFormat; } @@ -1201,15 +1329,38 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return new DummyTrackOutput(); } - private static final class PrivTimestampStrippingSampleQueue extends SampleQueue { + private static final class FormatAdjustingSampleQueue extends SampleQueue { - public PrivTimestampStrippingSampleQueue(Allocator allocator) { - super(allocator); + private final Map overridingDrmInitData; + @Nullable private DrmInitData drmInitData; + + public FormatAdjustingSampleQueue( + Allocator allocator, + Looper playbackLooper, + DrmSessionManager drmSessionManager, + Map overridingDrmInitData) { + super(allocator, playbackLooper, drmSessionManager); + this.overridingDrmInitData = overridingDrmInitData; + } + + public void setDrmInitData(@Nullable DrmInitData drmInitData) { + this.drmInitData = drmInitData; + invalidateUpstreamFormatAdjustment(); } @Override - public void format(Format format) { - super.format(format.copyWithMetadata(getAdjustedMetadata(format.metadata))); + public Format getAdjustedUpstreamFormat(Format format) { + @Nullable + DrmInitData drmInitData = this.drmInitData != null ? this.drmInitData : format.drmInitData; + if (drmInitData != null) { + @Nullable + DrmInitData overridingDrmInitData = this.overridingDrmInitData.get(drmInitData.schemeType); + if (overridingDrmInitData != null) { + drmInitData = overridingDrmInitData; + } + } + return super.getAdjustedUpstreamFormat( + format.copyWithAdjustments(drmInitData, getAdjustedMetadata(format.metadata))); } /** @@ -1270,14 +1421,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private byte[] buffer; private int bufferPosition; - public EmsgUnwrappingTrackOutput(TrackOutput delegate, @HlsMetadataType int metadataType) { + public EmsgUnwrappingTrackOutput( + TrackOutput delegate, @HlsMediaSource.MetadataType int metadataType) { this.emsgDecoder = new EventMessageDecoder(); this.delegate = delegate; switch (metadataType) { - case HlsMetadataType.ID3: + case HlsMediaSource.METADATA_TYPE_ID3: delegateFormat = ID3_FORMAT; break; - case HlsMetadataType.EMSG: + case HlsMediaSource.METADATA_TYPE_EMSG: delegateFormat = EMSG_FORMAT; break; default: @@ -1324,7 +1476,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; int size, int offset, @Nullable CryptoData cryptoData) { - Assertions.checkState(format != null); + Assertions.checkNotNull(format); ParsableByteArray sample = getSampleAndTrimBuffer(size, offset); ParsableByteArray sampleForDelegate; if (Util.areEqual(format.sampleMimeType, delegateFormat.sampleMimeType)) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java index 14268313e..f26a9b8e9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.source.hls; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.metadata.Metadata; import java.util.ArrayList; import java.util.Collections; @@ -184,6 +184,11 @@ public final class HlsTrackMetadataEntry implements Metadata.Entry { this.variantInfos = Collections.unmodifiableList(variantInfos); } + @Override + public String toString() { + return "HlsTrackMetadataEntry" + (groupId != null ? (" [" + groupId + ", " + name + "]") : ""); + } + @Override public boolean equals(@Nullable Object other) { if (this == other) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index 665f2e057..285ec1b6a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -26,6 +27,7 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.text.webvtt.WebvttParserUtil; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; @@ -33,6 +35,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * A special purpose extractor for WebVTT content in HLS. @@ -45,20 +49,20 @@ import java.util.regex.Pattern; public 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 static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(-?\\d+)"); private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */; private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH; - private final String language; + @Nullable private final String language; private final TimestampAdjuster timestampAdjuster; private final ParsableByteArray sampleDataWrapper; - private ExtractorOutput output; + private @MonotonicNonNull ExtractorOutput output; private byte[] sampleData; private int sampleSize; - public WebvttExtractor(String language, TimestampAdjuster timestampAdjuster) { + public WebvttExtractor(@Nullable String language, TimestampAdjuster timestampAdjuster) { this.language = language; this.timestampAdjuster = timestampAdjuster; this.sampleDataWrapper = new ParsableByteArray(); @@ -106,6 +110,8 @@ public final class WebvttExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + // output == null suggests init() hasn't been called + Assertions.checkNotNull(output); int currentFileSize = (int) input.getLength(); // Increase the size of sampleData if necessary. @@ -128,6 +134,7 @@ public final class WebvttExtractor implements Extractor { return Extractor.RESULT_END_OF_INPUT; } + @RequiresNonNull("output") private void processSample() throws ParserException { ParsableByteArray webvttData = new ParsableByteArray(sampleData); @@ -139,8 +146,9 @@ public final class WebvttExtractor implements Extractor { long tsTimestampUs = 0; // Parse the remainder of the header looking for X-TIMESTAMP-MAP. - String line; - while (!TextUtils.isEmpty(line = webvttData.readLine())) { + for (String line = webvttData.readLine(); + !TextUtils.isEmpty(line); + line = webvttData.readLine()) { if (line.startsWith("X-TIMESTAMP-MAP")) { Matcher localTimestampMatcher = LOCAL_TIMESTAMP.matcher(line); if (!localTimestampMatcher.find()) { @@ -175,6 +183,7 @@ public final class WebvttExtractor implements Extractor { trackOutput.sampleMetadata(sampleTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); } + @RequiresNonNull("output") private TrackOutput buildTrackOutput(long subsampleOffsetUs) { TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_TEXT); trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null, diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java new file mode 100644 index 000000000..252755382 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.source.hls.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java similarity index 61% rename from TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java rename to TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java index e445466e6..55f15f5e7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/HlsMetadataType.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/package-info.java @@ -12,23 +12,8 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ +@NonNullApi package com.google.android.exoplayer2.source.hls; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import androidx.annotation.IntDef; -import java.lang.annotation.Retention; - -/** - * The types of metadata that can be extracted from HLS streams. - * - *

See {@link HlsMediaSource.Factory#setMetadataType(int)}. - */ -@Retention(SOURCE) -@IntDef({HlsMetadataType.ID3, HlsMetadataType.EMSG}) -public @interface HlsMetadataType { - int ID3 = 1; - int EMSG = 3; -} +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index 4774264d6..f4fa2ad03 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -166,11 +166,13 @@ public final class DefaultHlsPlaylistTracker } @Override - public @Nullable HlsMasterPlaylist getMasterPlaylist() { + @Nullable + public HlsMasterPlaylist getMasterPlaylist() { return masterPlaylist; } @Override + @Nullable public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) { HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); if (snapshot != null && isForPlayback) { @@ -447,7 +449,7 @@ public final class DefaultHlsPlaylistTracker private final Loader mediaPlaylistLoader; private final ParsingLoadable mediaPlaylistLoadable; - private HlsMediaPlaylist playlistSnapshot; + @Nullable private HlsMediaPlaylist playlistSnapshot; private long lastSnapshotLoadMs; private long lastSnapshotChangeMs; private long earliestNextLoadTimeMs; @@ -466,6 +468,7 @@ public final class DefaultHlsPlaylistTracker mediaPlaylistParser); } + @Nullable public HlsMediaPlaylist getPlaylistSnapshot() { return playlistSnapshot; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index 0e86df8c2..f96c7dfa9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -174,13 +174,13 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * The format of the audio muxed in the variants. May be null if the playlist does not declare any * muxed audio. */ - public final Format muxedAudioFormat; + @Nullable public final Format muxedAudioFormat; /** * The format of the closed captions declared by the playlist. May be empty if the playlist * explicitly declares no captions are available, or null if the playlist does not declare any * captions information. */ - public final List muxedCaptionFormats; + @Nullable public final List muxedCaptionFormats; /** Contains variable definitions, as defined by the #EXT-X-DEFINE tag. */ public final Map variableDefinitions; /** DRM initialization data derived from #EXT-X-SESSION-KEY tags. */ @@ -208,8 +208,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { List audios, List subtitles, List closedCaptions, - Format muxedAudioFormat, - List muxedCaptionFormats, + @Nullable Format muxedAudioFormat, + @Nullable List muxedCaptionFormats, boolean hasIndependentSegments, Map variableDefinitions, List sessionKeyDrmInitData) { @@ -258,7 +258,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { List variant = Collections.singletonList(Variant.createMediaPlaylistVariantUrl(Uri.parse(variantUrl))); return new HlsMasterPlaylist( - /* baseUri= */ null, + /* baseUri= */ "", /* tags= */ Collections.emptyList(), variant, /* videos= */ Collections.emptyList(), diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java index 4411c9865..58f500cf9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls.playlist; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData; @@ -95,8 +94,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { String uri, long byterangeOffset, long byterangeLength, - String fullSegmentEncryptionKeyUri, - String encryptionIV) { + @Nullable String fullSegmentEncryptionKeyUri, + @Nullable String encryptionIV) { this( uri, /* initializationSegment= */ null, @@ -154,7 +153,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } @Override - public int compareTo(@NonNull Long relativeStartTimeUs) { + public int compareTo(Long relativeStartTimeUs) { return this.relativeStartTimeUs > relativeStartTimeUs ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 030520f8c..6bd447f74 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -48,10 +48,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Queue; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.PolyNull; /** @@ -302,12 +304,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants, String groupId) { for (int i = 0; i < variants.size(); i++) { Variant variant = variants.get(i); @@ -546,6 +568,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants, String groupId) { for (int i = 0; i < variants.size(); i++) { Variant variant = variants.get(i); @@ -556,6 +579,17 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variants, String groupId) { + for (int i = 0; i < variants.size(); i++) { + Variant variant = variants.get(i); + if (groupId.equals(variant.subtitleGroupId)) { + return variant; + } + } + return null; + } + private static HlsMediaPlaylist parseMediaPlaylist( HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; @@ -819,13 +853,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) { - String channelsString = parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions); - return channelsString != null - ? Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]) - : Format.NO_VALUE; - } - @Nullable private static SchemeData parseDrmSchemeData( String line, String keyFormat, Map variableDefinitions) @@ -859,6 +886,14 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser extraLines; - private String next; + @Nullable private String next; public LineIterator(Queue extraLines, BufferedReader reader) { this.extraLines = extraLines; this.reader = reader; } + @EnsuresNonNullIf(expression = "next", result = true) public boolean hasNext() throws IOException { if (next != null) { return true; } if (!extraLines.isEmpty()) { - next = extraLines.poll(); + next = Assertions.checkNotNull(extraLines.poll()); return true; } while ((next = reader.readLine()) != null) { @@ -955,13 +991,15 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser> { private final SsChunkSource.Factory chunkSourceFactory; - private final @Nullable TransferListener transferListener; + @Nullable private final TransferListener transferListener; private final LoaderErrorThrower manifestLoaderErrorThrower; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final EventDispatcher eventDispatcher; private final Allocator allocator; private final TrackGroupArray trackGroups; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private @Nullable Callback callback; + @Nullable private Callback callback; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; private SequenceableLoader compositeSequenceableLoader; @@ -61,6 +65,7 @@ import java.util.List; SsChunkSource.Factory chunkSourceFactory, @Nullable TransferListener transferListener, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, @@ -69,11 +74,12 @@ import java.util.List; this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; - trackGroups = buildTrackGroups(manifest); + trackGroups = buildTrackGroups(manifest, drmSessionManager); sampleStreams = newSampleStreamArray(0); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); @@ -115,8 +121,12 @@ import java.util.List; } @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + public long selectTracks( + @NullableType TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + @NullableType SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs) { ArrayList> sampleStreamsList = new ArrayList<>(); for (int i = 0; i < selections.length; i++) { if (streams[i] != null) { @@ -174,6 +184,11 @@ import java.util.List; return compositeSequenceableLoader.continueLoading(positionUs); } + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); + } + @Override public long getNextLoadPositionUs() { return compositeSequenceableLoader.getNextLoadPositionUs(); @@ -238,19 +253,32 @@ import java.util.List; this, allocator, positionUs, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher); } - private static TrackGroupArray buildTrackGroups(SsManifest manifest) { + private static TrackGroupArray buildTrackGroups( + SsManifest manifest, DrmSessionManager drmSessionManager) { TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length]; for (int i = 0; i < manifest.streamElements.length; i++) { - trackGroups[i] = new TrackGroup(manifest.streamElements[i].formats); + Format[] manifestFormats = manifest.streamElements[i].formats; + Format[] exposedFormats = new Format[manifestFormats.length]; + for (int j = 0; j < manifestFormats.length; j++) { + Format manifestFormat = manifestFormats[j]; + exposedFormats[j] = + manifestFormat.drmInitData != null + ? manifestFormat.copyWithExoMediaCryptoType( + drmSessionManager.getExoMediaCryptoType(manifestFormat.drmInitData)) + : manifestFormat; + } + trackGroups[i] = new TrackGroup(exposedFormats); } return new TrackGroupArray(trackGroups); } - @SuppressWarnings("unchecked") + // We won't assign the array to a variable that erases the generic type, and then write into it. + @SuppressWarnings({"unchecked", "rawtypes"}) private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 3f19b1b1b..89dd8039e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -22,6 +22,8 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.FilteringManifestParser; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BaseMediaSource; @@ -31,9 +33,9 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; -import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; @@ -61,7 +63,7 @@ public final class SsMediaSource extends BaseMediaSource } /** Factory for {@link SsMediaSource}. */ - public static final class Factory implements AdsMediaSource.MediaSourceFactory { + public static final class Factory implements MediaSourceFactory { private final SsChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; @@ -69,6 +71,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable private ParsingLoadable.Parser manifestParser; @Nullable private List streamKeys; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private DrmSessionManager drmSessionManager; private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private long livePresentationDelayMs; private boolean isCreateCalled; @@ -98,6 +101,7 @@ public final class SsMediaSource extends BaseMediaSource @Nullable DataSource.Factory manifestDataSourceFactory) { this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); @@ -111,7 +115,7 @@ public final class SsMediaSource extends BaseMediaSource * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Factory setTag(Object tag) { + public Factory setTag(@Nullable Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; return this; @@ -180,19 +184,6 @@ public final class SsMediaSource extends BaseMediaSource return this; } - /** - * Sets a list of {@link StreamKey stream keys} by which the manifest is filtered. - * - * @param streamKeys A list of {@link StreamKey stream keys}. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ - public Factory setStreamKeys(List streamKeys) { - Assertions.checkState(!isCreateCalled); - this.streamKeys = streamKeys; - return this; - } - /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source * loads data from multiple streams (video, audio etc.). The default is an instance of {@link @@ -233,6 +224,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, tag); @@ -254,6 +246,40 @@ public final class SsMediaSource extends BaseMediaSource return mediaSource; } + /** + * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, + * MediaSourceEventListener)} instead. + */ + @Deprecated + public SsMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + SsMediaSource mediaSource = createMediaSource(manifestUri); + if (eventHandler != null && eventListener != null) { + mediaSource.addEventListener(eventHandler, eventListener); + } + return mediaSource; + } + + /** + * Sets the {@link DrmSessionManager} to use for acquiring {@link DrmSession DrmSessions}. The + * default value is {@link DrmSessionManager#DUMMY}. + * + * @param drmSessionManager The {@link DrmSessionManager}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + @Override + public Factory setDrmSessionManager(DrmSessionManager drmSessionManager) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManager = + drmSessionManager != null + ? drmSessionManager + : DrmSessionManager.getDummyDrmSessionManager(); + return this; + } + /** * Returns a new {@link SsMediaSource} using the current parameters. * @@ -276,25 +302,17 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, livePresentationDelayMs, tag); } - /** - * @deprecated Use {@link #createMediaSource(Uri)} and {@link #addEventListener(Handler, - * MediaSourceEventListener)} instead. - */ - @Deprecated - public SsMediaSource createMediaSource( - Uri manifestUri, - @Nullable Handler eventHandler, - @Nullable MediaSourceEventListener eventListener) { - SsMediaSource mediaSource = createMediaSource(manifestUri); - if (eventHandler != null && eventListener != null) { - mediaSource.addEventListener(eventHandler, eventListener); - } - return mediaSource; + @Override + public Factory setStreamKeys(List streamKeys) { + Assertions.checkState(!isCreateCalled); + this.streamKeys = streamKeys; + return this; } @Override @@ -324,17 +342,18 @@ public final class SsMediaSource extends BaseMediaSource private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final DrmSessionManager drmSessionManager; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final long livePresentationDelayMs; private final EventDispatcher manifestEventDispatcher; private final ParsingLoadable.Parser manifestParser; private final ArrayList mediaPeriods; - private final @Nullable Object tag; + @Nullable private final Object tag; private DataSource manifestDataSource; private Loader manifestLoader; private LoaderErrorThrower manifestLoaderErrorThrower; - private @Nullable TransferListener mediaTransferListener; + @Nullable private TransferListener mediaTransferListener; private long manifestLoadStartTimestamp; private SsManifest manifest; @@ -355,8 +374,8 @@ public final class SsMediaSource extends BaseMediaSource public SsMediaSource( SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, chunkSourceFactory, @@ -380,8 +399,8 @@ public final class SsMediaSource extends BaseMediaSource SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifest, /* manifestUri= */ null, @@ -389,6 +408,7 @@ public final class SsMediaSource extends BaseMediaSource /* manifestParser= */ null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), DEFAULT_LIVE_PRESENTATION_DELAY_MS, /* tag= */ null); @@ -415,8 +435,8 @@ public final class SsMediaSource extends BaseMediaSource Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( manifestUri, manifestDataSourceFactory, @@ -450,8 +470,8 @@ public final class SsMediaSource extends BaseMediaSource SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -480,8 +500,8 @@ public final class SsMediaSource extends BaseMediaSource SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, - Handler eventHandler, - MediaSourceEventListener eventListener) { + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { this( /* manifest= */ null, manifestUri, @@ -489,6 +509,7 @@ public final class SsMediaSource extends BaseMediaSource manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), + DrmSessionManager.getDummyDrmSessionManager(), new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount), livePresentationDelayMs, /* tag= */ null); @@ -498,12 +519,13 @@ public final class SsMediaSource extends BaseMediaSource } private SsMediaSource( - SsManifest manifest, - Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, - ParsingLoadable.Parser manifestParser, + @Nullable SsManifest manifest, + @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + @Nullable ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long livePresentationDelayMs, @Nullable Object tag) { @@ -514,6 +536,7 @@ public final class SsMediaSource extends BaseMediaSource this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.drmSessionManager = drmSessionManager; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.livePresentationDelayMs = livePresentationDelayMs; this.manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); @@ -531,8 +554,9 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; + drmSessionManager.prepare(); if (sideloadedManifest) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); processManifest(); @@ -559,6 +583,7 @@ public final class SsMediaSource extends BaseMediaSource chunkSourceFactory, mediaTransferListener, compositeSequenceableLoaderFactory, + drmSessionManager, loadErrorHandlingPolicy, eventDispatcher, manifestLoaderErrorThrower, @@ -574,7 +599,7 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public void releaseSourceInternal() { + protected void releaseSourceInternal() { manifest = sideloadedManifest ? manifest : null; manifestDataSource = null; manifestLoadStartTimestamp = 0; @@ -586,6 +611,7 @@ public final class SsMediaSource extends BaseMediaSource manifestRefreshHandler.removeCallbacksAndMessages(null); manifestRefreshHandler = null; } + drmSessionManager.release(); } // Loader.Callback implementation @@ -674,7 +700,9 @@ public final class SsMediaSource extends BaseMediaSource /* windowPositionInPeriodUs= */ 0, /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, - manifest.isLive, + /* isDynamic= */ manifest.isLive, + /* isLive= */ manifest.isLive, + manifest, tag); } else if (manifest.isLive) { if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) { @@ -696,6 +724,8 @@ public final class SsMediaSource extends BaseMediaSource defaultStartPositionUs, /* isSeekable= */ true, /* isDynamic= */ true, + /* isLive= */ true, + manifest, tag); } else { long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs @@ -708,9 +738,11 @@ public final class SsMediaSource extends BaseMediaSource /* windowDefaultStartPositionUs= */ 0, /* isSeekable= */ true, /* isDynamic= */ false, + /* isLive= */ false, + manifest, tag); } - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); } private void scheduleManifestRefresh() { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index cfb772a86..b91bfc8f6 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; import android.net.Uri; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; @@ -69,7 +70,7 @@ public class SsManifest implements FilterableManifest { public final int maxHeight; public final int displayWidth; public final int displayHeight; - public final String language; + @Nullable public final String language; public final Format[] formats; public final int chunkCount; @@ -80,9 +81,20 @@ public class SsManifest implements FilterableManifest { private final long[] chunkStartTimesUs; private final long lastChunkDurationUs; - public StreamElement(String baseUri, String chunkTemplate, int type, String subType, - long timescale, String name, int maxWidth, int maxHeight, int displayWidth, - int displayHeight, String language, Format[] formats, List chunkStartTimes, + public StreamElement( + String baseUri, + String chunkTemplate, + int type, + String subType, + long timescale, + String name, + int maxWidth, + int maxHeight, + int displayWidth, + int displayHeight, + @Nullable String language, + Format[] formats, + List chunkStartTimes, long lastChunkDuration) { this( baseUri, @@ -102,10 +114,22 @@ public class SsManifest implements FilterableManifest { Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale)); } - private StreamElement(String baseUri, String chunkTemplate, int type, String subType, - long timescale, String name, int maxWidth, int maxHeight, int displayWidth, - int displayHeight, String language, Format[] formats, List chunkStartTimes, - long[] chunkStartTimesUs, long lastChunkDurationUs) { + private StreamElement( + String baseUri, + String chunkTemplate, + int type, + String subType, + long timescale, + String name, + int maxWidth, + int maxHeight, + int displayWidth, + int displayHeight, + @Nullable String language, + Format[] formats, + List chunkStartTimes, + long[] chunkStartTimesUs, + long lastChunkDurationUs) { this.baseUri = baseUri; this.chunkTemplate = chunkTemplate; this.type = type; @@ -208,7 +232,7 @@ public class SsManifest implements FilterableManifest { public final boolean isLive; /** Content protection information, or null if the content is not protected. */ - public final ProtectionElement protectionElement; + @Nullable public final ProtectionElement protectionElement; /** The contained stream elements. */ public final StreamElement[] streamElements; @@ -249,7 +273,7 @@ public class SsManifest implements FilterableManifest { long dvrWindowLength, int lookAheadCount, boolean isLive, - ProtectionElement protectionElement, + @Nullable ProtectionElement protectionElement, StreamElement[] streamElements) { this( majorVersion, @@ -273,7 +297,7 @@ public class SsManifest implements FilterableManifest { long dvrWindowLengthUs, int lookAheadCount, boolean isLive, - ProtectionElement protectionElement, + @Nullable ProtectionElement protectionElement, StreamElement[] streamElements) { this.majorVersion = majorVersion; this.minorVersion = minorVersion; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 9d7ee56fc..d395e95fd 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Base64; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; @@ -40,6 +41,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.UUID; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -94,10 +96,10 @@ public class SsManifestParser implements ParsingLoadable.Parser { private final String baseUri; private final String tag; - private final ElementParser parent; - private final List> normalizedAttributes; + @Nullable private final ElementParser parent; + private final List> normalizedAttributes; - public ElementParser(ElementParser parent, String baseUri, String tag) { + public ElementParser(@Nullable ElementParser parent, String baseUri, String tag) { this.parent = parent; this.baseUri = baseUri; this.tag = tag; @@ -174,24 +176,25 @@ public class SsManifestParser implements ParsingLoadable.Parser { * 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 + * + *

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) { + protected final void putNormalizedAttribute(String key, @Nullable 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. + * 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. */ + @Nullable protected final Object getNormalizedAttribute(String key) { for (int i = 0; i < normalizedAttributes.size(); i++) { Pair pair = normalizedAttributes.get(i); @@ -340,7 +343,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { private long dvrWindowLength; private int lookAheadCount; private boolean isLive; - private ProtectionElement protectionElement; + @Nullable private ProtectionElement protectionElement; public SmoothStreamingMediaParser(ElementParser parent, String baseUri) { super(parent, baseUri, TAG); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java new file mode 100644 index 000000000..b594ddc2b --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.source.smoothstreaming.manifest; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java new file mode 100644 index 000000000..f7c74f1a1 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.source.smoothstreaming.offline; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java new file mode 100644 index 000000000..23e85850c --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.source.smoothstreaming; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java index b863d80c9..51aec3638 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/CaptionStyleCompat.java @@ -18,9 +18,10 @@ package com.google.android.exoplayer2.text; import android.annotation.TargetApi; import android.graphics.Color; import android.graphics.Typeface; -import androidx.annotation.IntDef; import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager.CaptionStyle; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -72,11 +73,15 @@ public final class CaptionStyleCompat { */ 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); + /** Default caption style. */ + public static final CaptionStyleCompat DEFAULT = + new CaptionStyleCompat( + Color.WHITE, + Color.BLACK, + Color.TRANSPARENT, + EDGE_TYPE_NONE, + Color.WHITE, + /* typeface= */ null); /** * The preferred foreground color. @@ -110,10 +115,8 @@ public final class CaptionStyleCompat { */ public final int edgeColor; - /** - * The preferred typeface. - */ - public final Typeface typeface; + /** The preferred typeface, or {@code null} if unspecified. */ + @Nullable public final Typeface typeface; /** * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. @@ -141,8 +144,13 @@ public final class CaptionStyleCompat { * @param edgeColor See {@link #edgeColor}. * @param typeface See {@link #typeface}. */ - public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, - @EdgeType int edgeType, int edgeColor, Typeface typeface) { + public CaptionStyleCompat( + int foregroundColor, + int backgroundColor, + int windowColor, + @EdgeType int edgeType, + int edgeColor, + @Nullable Typeface typeface) { this.foregroundColor = foregroundColor; this.backgroundColor = backgroundColor; this.windowColor = windowColor; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/Cue.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/Cue.java index 3f6ff4424..946af76e5 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/Cue.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/Cue.java @@ -17,8 +17,9 @@ package com.google.android.exoplayer2.text; import android.graphics.Bitmap; import android.graphics.Color; -import androidx.annotation.IntDef; import android.text.Layout.Alignment; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -31,8 +32,9 @@ public class Cue { /** The empty cue. */ public static final Cue EMPTY = new Cue(""); - /** An unset position or width. */ - public static final float DIMEN_UNSET = Float.MIN_VALUE; + /** An unset position, width or size. */ + // Note: We deliberately don't use Float.MIN_VALUE because it's positive & very close to zero. + public static final float DIMEN_UNSET = -Float.MAX_VALUE; /** * The type of anchor, which may be unset. One of {@link #TYPE_UNSET}, {@link #ANCHOR_TYPE_START}, @@ -112,17 +114,13 @@ public class Cue { * The cue text, or null if this is an image cue. Note the {@link CharSequence} may be decorated * with styling spans. */ - public final CharSequence text; + @Nullable public final CharSequence text; - /** - * The alignment of the cue text within the cue box, or null if the alignment is undefined. - */ - public final Alignment textAlignment; + /** The alignment of the cue text within the cue box, or null if the alignment is undefined. */ + @Nullable public final Alignment textAlignment; - /** - * The cue image, or null if this is a text cue. - */ - public final Bitmap bitmap; + /** The cue image, or null if this is a text cue. */ + @Nullable public final Bitmap bitmap; /** * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction @@ -299,7 +297,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -335,7 +333,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -377,7 +375,7 @@ public class Cue { */ public Cue( CharSequence text, - Alignment textAlignment, + @Nullable Alignment textAlignment, float line, @LineType int lineType, @AnchorType int lineAnchor, @@ -404,9 +402,9 @@ public class Cue { } private Cue( - CharSequence text, - Alignment textAlignment, - Bitmap bitmap, + @Nullable CharSequence text, + @Nullable Alignment textAlignment, + @Nullable Bitmap bitmap, float line, @LineType int lineType, @AnchorType int lineAnchor, diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java index bd561afaf..8a1aea179 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.SimpleDecoder; +import com.google.android.exoplayer2.util.Assertions; import java.nio.ByteBuffer; /** @@ -29,9 +30,8 @@ public abstract class SimpleSubtitleDecoder extends private final String name; - /** - * @param name The name of the decoder. - */ + /** @param name The name of the decoder. */ + @SuppressWarnings("initialization:method.invocation.invalid") protected SimpleSubtitleDecoder(String name) { super(new SubtitleInputBuffer[2], new SubtitleOutputBuffer[2]); this.name = name; @@ -74,7 +74,7 @@ public abstract class SimpleSubtitleDecoder extends protected final SubtitleDecoderException decode( SubtitleInputBuffer inputBuffer, SubtitleOutputBuffer outputBuffer, boolean reset) { try { - ByteBuffer inputData = inputBuffer.data; + ByteBuffer inputData = Assertions.checkNotNull(inputBuffer.data); Subtitle subtitle = decode(inputData.array(), inputData.limit(), reset); outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs); // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]). diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index a64a1835d..927ee8be5 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.cea.Cea608Decoder; import com.google.android.exoplayer2.text.cea.Cea708Decoder; @@ -74,7 +75,7 @@ public interface SubtitleDecoderFactory { @Override public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; + @Nullable String mimeType = format.sampleMimeType; return MimeTypes.TEXT_VTT.equals(mimeType) || MimeTypes.TEXT_SSA.equals(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType) @@ -90,32 +91,36 @@ public interface SubtitleDecoderFactory { @Override public SubtitleDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); - case MimeTypes.TEXT_SSA: - return new SsaDecoder(format.initializationData); - case MimeTypes.APPLICATION_MP4VTT: - return new Mp4WebvttDecoder(); - case MimeTypes.APPLICATION_TTML: - return new TtmlDecoder(); - case MimeTypes.APPLICATION_SUBRIP: - return new SubripDecoder(); - case MimeTypes.APPLICATION_TX3G: - return new Tx3gDecoder(format.initializationData); - case MimeTypes.APPLICATION_CEA608: - case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); - case MimeTypes.APPLICATION_CEA708: - return new Cea708Decoder(format.accessibilityChannel, format.initializationData); - case MimeTypes.APPLICATION_DVBSUBS: - return new DvbDecoder(format.initializationData); - case MimeTypes.APPLICATION_PGS: - return new PgsDecoder(); - default: - throw new IllegalArgumentException( - "Attempted to create decoder for unsupported format"); + @Nullable String mimeType = format.sampleMimeType; + if (mimeType != null) { + switch (mimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + case MimeTypes.TEXT_SSA: + return new SsaDecoder(format.initializationData); + case MimeTypes.APPLICATION_MP4VTT: + return new Mp4WebvttDecoder(); + case MimeTypes.APPLICATION_TTML: + return new TtmlDecoder(); + case MimeTypes.APPLICATION_SUBRIP: + return new SubripDecoder(); + case MimeTypes.APPLICATION_TX3G: + return new Tx3gDecoder(format.initializationData); + case MimeTypes.APPLICATION_CEA608: + case MimeTypes.APPLICATION_MP4CEA608: + return new Cea608Decoder(mimeType, format.accessibilityChannel); + case MimeTypes.APPLICATION_CEA708: + return new Cea708Decoder(format.accessibilityChannel, format.initializationData); + case MimeTypes.APPLICATION_DVBSUBS: + return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); + default: + break; + } } + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported MIME type: " + mimeType); } }; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java index 843cfab04..1dcdecf95 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.OutputBuffer; import com.google.android.exoplayer2.util.Assertions; @@ -25,7 +26,7 @@ import java.util.List; */ public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle { - private Subtitle subtitle; + @Nullable private Subtitle subtitle; private long subsampleOffsetUs; /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 55bee5bd6..46c26db12 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -23,10 +23,11 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; @@ -44,11 +45,7 @@ import java.util.List; */ public final class TextRenderer extends BaseRenderer implements Callback { - /** - * @deprecated Use {@link TextOutput}. - */ - @Deprecated - public interface Output extends TextOutput {} + private static final String TAG = "TextRenderer"; @Documented @Retention(RetentionPolicy.SOURCE) @@ -77,7 +74,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { private static final int MSG_UPDATE_OUTPUT = 0; - private final @Nullable Handler outputHandler; + @Nullable private final Handler outputHandler; private final TextOutput output; private final SubtitleDecoderFactory decoderFactory; private final FormatHolder formatHolder; @@ -85,11 +82,11 @@ public final class TextRenderer extends BaseRenderer implements Callback { private boolean inputStreamEnded; private boolean outputStreamEnded; @ReplacementState private int decoderReplacementState; - private Format streamFormat; - private SubtitleDecoder decoder; - private SubtitleInputBuffer nextInputBuffer; - private SubtitleOutputBuffer subtitle; - private SubtitleOutputBuffer nextSubtitle; + @Nullable private Format streamFormat; + @Nullable private SubtitleDecoder decoder; + @Nullable private SubtitleInputBuffer nextInputBuffer; + @Nullable private SubtitleOutputBuffer subtitle; + @Nullable private SubtitleOutputBuffer nextSubtitle; private int nextSubtitleEventIndex; /** @@ -124,18 +121,20 @@ public final class TextRenderer extends BaseRenderer implements Callback { } @Override + @Capabilities public int supportsFormat(Format format) { if (decoderFactory.supportsFormat(format)) { - return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + return RendererCapabilities.create( + supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM); } else if (MimeTypes.isText(format.sampleMimeType)) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); } else { - return FORMAT_UNSUPPORTED_TYPE; + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } } @Override - protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + protected void onStreamChanged(Format[] formats, long offsetUs) { streamFormat = formats[0]; if (decoder != null) { decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM; @@ -146,19 +145,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onPositionReset(long positionUs, boolean joining) { - clearOutput(); inputStreamEnded = false; outputStreamEnded = false; - if (decoderReplacementState != REPLACEMENT_STATE_NONE) { - replaceDecoder(); - } else { - releaseBuffers(); - decoder.flush(); - } + resetOutputAndDecoder(); } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (outputStreamEnded) { return; } @@ -168,7 +161,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { try { nextSubtitle = decoder.dequeueOutputBuffer(); } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + handleDecoderError(e); + return; } } @@ -250,7 +244,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } } catch (SubtitleDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + handleDecoderError(e); + return; } } @@ -332,4 +327,24 @@ public final class TextRenderer extends BaseRenderer implements Callback { output.onCues(cues); } + /** + * Called when {@link #decoder} throws an exception, so it can be logged and playback can + * continue. + * + *

Logs {@code e} and resets state to allow decoding the next sample. + */ + private void handleDecoderError(SubtitleDecoderException e) { + Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e); + resetOutputAndDecoder(); + } + + private void resetOutputAndDecoder() { + clearOutput(); + if (decoderReplacementState != REPLACEMENT_STATE_NONE) { + replaceDecoder(); + } else { + releaseBuffers(); + decoder.flush(); + } + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java index a0201e19e..e04094a8d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Cue.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.text.cea; -import androidx.annotation.NonNull; import android.text.Layout.Alignment; +import androidx.annotation.NonNull; import com.google.android.exoplayer2.text.Cue; /** @@ -24,11 +24,6 @@ import com.google.android.exoplayer2.text.Cue; */ /* package */ final class Cea708Cue extends Cue implements Comparable { - /** - * An unset priority. - */ - public static final int PRIORITY_UNSET = -1; - /** * The priority of the cue box. */ @@ -64,5 +59,4 @@ import com.google.android.exoplayer2.text.Cue; } return 0; } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index b3be88b85..4391bc0bf 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -25,6 +25,7 @@ import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Cue; @@ -152,7 +153,8 @@ public final class Cea708Decoder extends CeaDecoder { private DtvCcPacket currentDtvCcPacket; private int currentWindow; - public Cea708Decoder(int accessibilityChannel, List initializationData) { + // TODO: Retrieve isWideAspectRatio from initializationData and use it. + public Cea708Decoder(int accessibilityChannel, @Nullable List initializationData) { ccData = new ParsableByteArray(); serviceBlockPacket = new ParsableBitArray(); selectedServiceNumber = accessibilityChannel == Format.NO_VALUE ? 1 : accessibilityChannel; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java index 75fe8fed2..cdc545e45 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java @@ -19,14 +19,13 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; -import com.google.android.exoplayer2.util.Util; /** Utility methods for handling CEA-608/708 messages. Defined in A/53 Part 4:2009. */ public final class CeaUtil { private static final String TAG = "CeaUtil"; - public static final int USER_DATA_IDENTIFIER_GA94 = Util.getIntegerCodeForString("GA94"); + public static final int USER_DATA_IDENTIFIER_GA94 = 0x47413934; public static final int USER_DATA_TYPE_CODE_MPEG_CC = 0x3; private static final int PAYLOAD_TYPE_CC = 4; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java new file mode 100644 index 000000000..cbdf178b6 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/cea/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.text.cea; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java index df5b19c05..22ce893fc 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.text.dvb; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; @@ -38,7 +39,7 @@ public final class DvbDecoder extends SimpleSubtitleDecoder { } @Override - protected DvbSubtitle decode(byte[] data, int length, boolean reset) { + protected Subtitle decode(byte[] data, int length, boolean reset) { if (reset) { parser.reset(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java index 3f2fef454..8382d9d9d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/DvbParser.java @@ -22,6 +22,7 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableBitArray; @@ -29,6 +30,7 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Parses {@link Cue}s from a DVB subtitle bitstream. @@ -85,7 +87,7 @@ import java.util.List; private final ClutDefinition defaultClutDefinition; private final SubtitleService subtitleService; - private Bitmap bitmap; + @MonotonicNonNull private Bitmap bitmap; /** * Construct an instance for the given subtitle and ancillary page ids. @@ -131,7 +133,8 @@ import java.util.List; parseSubtitlingSegment(dataBitArray, subtitleService); } - if (subtitleService.pageComposition == null) { + @Nullable PageComposition pageComposition = subtitleService.pageComposition; + if (pageComposition == null) { return Collections.emptyList(); } @@ -147,7 +150,7 @@ import java.util.List; // Build the cues. List cues = new ArrayList<>(); - SparseArray pageRegions = subtitleService.pageComposition.regions; + SparseArray pageRegions = pageComposition.regions; for (int i = 0; i < pageRegions.size(); i++) { // Save clean clipping state. canvas.save(); @@ -182,7 +185,7 @@ import java.util.List; objectData = subtitleService.ancillaryObjects.get(objectId); } if (objectData != null) { - Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; + @Nullable Paint paint = objectData.nonModifyingColorFlag ? null : defaultPaint; paintPixelDataSubBlocks(objectData, clutDefinition, regionComposition.depth, baseHorizontalAddress + regionObject.horizontalPosition, baseVerticalAddress + regionObject.verticalPosition, paint, canvas); @@ -248,7 +251,7 @@ import java.util.List; break; case SEGMENT_TYPE_PAGE_COMPOSITION: if (pageId == service.subtitlePageId) { - PageComposition current = service.pageComposition; + @Nullable PageComposition current = service.pageComposition; PageComposition pageComposition = parsePageComposition(data, dataFieldLength); if (pageComposition.state != PAGE_STATE_NORMAL) { service.pageComposition = pageComposition; @@ -261,11 +264,15 @@ import java.util.List; } break; case SEGMENT_TYPE_REGION_COMPOSITION: - PageComposition pageComposition = service.pageComposition; + @Nullable PageComposition pageComposition = service.pageComposition; if (pageId == service.subtitlePageId && pageComposition != null) { RegionComposition regionComposition = parseRegionComposition(data, dataFieldLength); if (pageComposition.state == PAGE_STATE_NORMAL) { - regionComposition.mergeFrom(service.regions.get(regionComposition.id)); + @Nullable + RegionComposition existingRegionComposition = service.regions.get(regionComposition.id); + if (existingRegionComposition != null) { + regionComposition.mergeFrom(existingRegionComposition); + } } service.regions.put(regionComposition.id, regionComposition); } @@ -470,8 +477,8 @@ import java.util.List; boolean nonModifyingColorFlag = data.readBit(); data.skipBits(1); // Skip reserved. - byte[] topFieldData = null; - byte[] bottomFieldData = null; + @Nullable byte[] topFieldData = null; + @Nullable byte[] bottomFieldData = null; if (objectCodingMethod == OBJECT_CODING_STRING) { int numberOfCodes = data.readBits(8); @@ -577,11 +584,15 @@ import java.util.List; // Static drawing. - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlocks(ObjectData objectData, ClutDefinition clutDefinition, - int regionDepth, int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlocks( + ObjectData objectData, + ClutDefinition clutDefinition, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { int[] clutEntries; if (regionDepth == REGION_DEPTH_8_BIT) { clutEntries = clutDefinition.clutEntries8Bit; @@ -596,23 +607,27 @@ import java.util.List; verticalAddress + 1, paint, canvas); } - /** - * Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. - */ - private static void paintPixelDataSubBlock(byte[] pixelData, int[] clutEntries, int regionDepth, - int horizontalAddress, int verticalAddress, Paint paint, Canvas canvas) { + /** Draws a pixel data sub-block, as defined by ETSI EN 300 743 7.2.5.1, into a canvas. */ + private static void paintPixelDataSubBlock( + byte[] pixelData, + int[] clutEntries, + int regionDepth, + int horizontalAddress, + int verticalAddress, + @Nullable Paint paint, + Canvas canvas) { ParsableBitArray data = new ParsableBitArray(pixelData); int column = horizontalAddress; int line = verticalAddress; - byte[] clutMapTable2To4 = null; - byte[] clutMapTable2To8 = null; - byte[] clutMapTable4To8 = null; + @Nullable byte[] clutMapTable2To4 = null; + @Nullable byte[] clutMapTable2To8 = null; + @Nullable byte[] clutMapTable4To8 = null; while (data.bitsLeft() != 0) { int dataType = data.readBits(8); switch (dataType) { case DATA_TYPE_2BP_CODE_STRING: - byte[] clutMapTable2ToX; + @Nullable byte[] clutMapTable2ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable2ToX = clutMapTable2To8 == null ? defaultMap2To8 : clutMapTable2To8; } else if (regionDepth == REGION_DEPTH_4_BIT) { @@ -625,7 +640,7 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_4BP_CODE_STRING: - byte[] clutMapTable4ToX; + @Nullable byte[] clutMapTable4ToX; if (regionDepth == REGION_DEPTH_8_BIT) { clutMapTable4ToX = clutMapTable4To8 == null ? defaultMap4To8 : clutMapTable4To8; } else { @@ -636,7 +651,9 @@ import java.util.List; data.byteAlign(); break; case DATA_TYPE_8BP_CODE_STRING: - column = paint8BitPixelCodeString(data, clutEntries, null, column, line, paint, canvas); + column = + paint8BitPixelCodeString( + data, clutEntries, /* clutMapTable= */ null, column, line, paint, canvas); break; case DATA_TYPE_24_TABLE_DATA: clutMapTable2To4 = buildClutMapTable(4, 4, data); @@ -645,7 +662,7 @@ import java.util.List; clutMapTable2To8 = buildClutMapTable(4, 8, data); break; case DATA_TYPE_48_TABLE_DATA: - clutMapTable2To8 = buildClutMapTable(16, 8, data); + clutMapTable4To8 = buildClutMapTable(16, 8, data); break; case DATA_TYPE_END_LINE: column = horizontalAddress; @@ -658,11 +675,15 @@ import java.util.List; } } - /** - * Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint2BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 2-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint2BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -706,11 +727,15 @@ import java.util.List; return column; } - /** - * Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint4BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint a 4-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint4BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -760,11 +785,15 @@ import java.util.List; return column; } - /** - * Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. - */ - private static int paint8BitPixelCodeString(ParsableBitArray data, int[] clutEntries, - byte[] clutMapTable, int column, int line, Paint paint, Canvas canvas) { + /** Paint an 8-bit/pixel code string, as defined by ETSI EN 300 743 7.2.5.2, to a canvas. */ + private static int paint8BitPixelCodeString( + ParsableBitArray data, + int[] clutEntries, + @Nullable byte[] clutMapTable, + int column, + int line, + @Nullable Paint paint, + Canvas canvas) { boolean endOfPixelCodeString = false; do { int runLength = 0; @@ -816,18 +845,23 @@ import java.util.List; public final int subtitlePageId; public final int ancillaryPageId; - public final SparseArray regions = new SparseArray<>(); - public final SparseArray cluts = new SparseArray<>(); - public final SparseArray objects = new SparseArray<>(); - public final SparseArray ancillaryCluts = new SparseArray<>(); - public final SparseArray ancillaryObjects = new SparseArray<>(); + public final SparseArray regions; + public final SparseArray cluts; + public final SparseArray objects; + public final SparseArray ancillaryCluts; + public final SparseArray ancillaryObjects; - public DisplayDefinition displayDefinition; - public PageComposition pageComposition; + @Nullable public DisplayDefinition displayDefinition; + @Nullable public PageComposition pageComposition; public SubtitleService(int subtitlePageId, int ancillaryPageId) { this.subtitlePageId = subtitlePageId; this.ancillaryPageId = ancillaryPageId; + regions = new SparseArray<>(); + cluts = new SparseArray<>(); + objects = new SparseArray<>(); + ancillaryCluts = new SparseArray<>(); + ancillaryObjects = new SparseArray<>(); } public void reset() { @@ -944,9 +978,6 @@ import java.util.List; } public void mergeFrom(RegionComposition otherRegionComposition) { - if (otherRegionComposition == null) { - return; - } SparseArray otherRegionObjects = otherRegionComposition.regionObjects; for (int i = 0; i < otherRegionObjects.size(); i++) { regionObjects.put(otherRegionObjects.keyAt(i), otherRegionObjects.valueAt(i)); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java new file mode 100644 index 000000000..e5ec87a1a --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/dvb/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.text.dvb; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/package-info.java new file mode 100644 index 000000000..5c5b3bbc3 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.text; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/pgs/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/pgs/package-info.java new file mode 100644 index 000000000..ff0819d99 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/pgs/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.text.pgs; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index b1af75f61..eef9d2eec 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -15,78 +15,100 @@ */ package com.google.android.exoplayer2.text.ssa; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import android.text.Layout; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * A {@link SimpleSubtitleDecoder} for SSA/ASS. - */ +/** A {@link SimpleSubtitleDecoder} for SSA/ASS. */ public final class SsaDecoder extends SimpleSubtitleDecoder { private static final String TAG = "SsaDecoder"; - private static final Pattern SSA_TIMECODE_PATTERN = Pattern.compile( - "(?:(\\d+):)?(\\d+):(\\d+)(?::|\\.)(\\d+)"); - private static final String FORMAT_LINE_PREFIX = "Format: "; - private static final String DIALOGUE_LINE_PREFIX = "Dialogue: "; + private static final Pattern SSA_TIMECODE_PATTERN = + Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+)[:.](\\d+)"); + + /* package */ static final String FORMAT_LINE_PREFIX = "Format:"; + /* package */ static final String STYLE_LINE_PREFIX = "Style:"; + private static final String DIALOGUE_LINE_PREFIX = "Dialogue:"; + + private static final float DEFAULT_MARGIN = 0.05f; private final boolean haveInitializationData; + @Nullable private final SsaDialogueFormat dialogueFormatFromInitializationData; - private int formatKeyCount; - private int formatStartIndex; - private int formatEndIndex; - private int formatTextIndex; + private @MonotonicNonNull Map styles; + + /** + * The horizontal resolution used by the subtitle author - all cue positions are relative to this. + * + *

Parsed from the {@code PlayResX} value in the {@code [Script Info]} section. + */ + private float screenWidth; + /** + * The vertical resolution used by the subtitle author - all cue positions are relative to this. + * + *

Parsed from the {@code PlayResY} value in the {@code [Script Info]} section. + */ + private float screenHeight; public SsaDecoder() { this(/* initializationData= */ null); } /** + * Constructs an SsaDecoder with optional format and header info. + * * @param initializationData Optional initialization data for the decoder. If not null or empty, * the initialization data must consist of two byte arrays. The first must contain an SSA * format line. The second must contain an SSA header that will be assumed common to all - * samples. + * samples. The header is everything in an SSA file before the {@code [Events]} section (i.e. + * {@code [Script Info]} and optional {@code [V4+ Styles]} section. */ public SsaDecoder(@Nullable List initializationData) { super("SsaDecoder"); + screenWidth = Cue.DIMEN_UNSET; + screenHeight = Cue.DIMEN_UNSET; + if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; String formatLine = Util.fromUtf8Bytes(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); - parseFormatLine(formatLine); + dialogueFormatFromInitializationData = + Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine)); parseHeader(new ParsableByteArray(initializationData.get(1))); } else { haveInitializationData = false; + dialogueFormatFromInitializationData = null; } } @Override - protected SsaSubtitle decode(byte[] bytes, int length, boolean reset) { - ArrayList cues = new ArrayList<>(); - LongArray cueTimesUs = new LongArray(); + protected Subtitle decode(byte[] bytes, int length, boolean reset) { + List> cues = new ArrayList<>(); + List cueTimesUs = new ArrayList<>(); ParsableByteArray data = new ParsableByteArray(bytes, length); if (!haveInitializationData) { parseHeader(data); } parseEventBody(data, cues, cueTimesUs); - - Cue[] cuesArray = new Cue[cues.size()]; - cues.toArray(cuesArray); - long[] cueTimesUsArray = cueTimesUs.toArray(); - return new SsaSubtitle(cuesArray, cueTimesUsArray); + return new SsaSubtitle(cues, cueTimesUs); } /** @@ -95,115 +117,160 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param data A {@link ParsableByteArray} from which the header should be read. */ private void parseHeader(ParsableByteArray data) { - String currentLine; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null) { - // TODO: Parse useful data from the header. - if (currentLine.startsWith("[Events]")) { - // We've reached the event body. + if ("[Script Info]".equalsIgnoreCase(currentLine)) { + parseScriptInfo(data); + } else if ("[V4+ Styles]".equalsIgnoreCase(currentLine)) { + styles = parseStyles(data); + } else if ("[V4 Styles]".equalsIgnoreCase(currentLine)) { + Log.i(TAG, "[V4 Styles] are not supported"); + } else if ("[Events]".equalsIgnoreCase(currentLine)) { + // We've reached the [Events] section, so the header is over. return; } } } + /** + * Parse the {@code [Script Info]} section. + * + *

When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition() position} + * set to the beginning of of the first line after {@code [Script Info]}. + */ + private void parseScriptInfo(ParsableByteArray data) { + @Nullable String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + String[] infoNameAndValue = currentLine.split(":"); + if (infoNameAndValue.length != 2) { + continue; + } + switch (Util.toLowerInvariant(infoNameAndValue[0].trim())) { + case "playresx": + try { + screenWidth = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResX value. + } + break; + case "playresy": + try { + screenHeight = Float.parseFloat(infoNameAndValue[1].trim()); + } catch (NumberFormatException e) { + // Ignore invalid PlayResY value. + } + break; + } + } + } + + /** + * Parse the {@code [V4+ Styles]} section. + * + *

When this returns, {@code data.position} will be set to the beginning of the first line that + * starts with {@code [} (i.e. the title of the next section). + * + * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing + * at the beginning of of the first line after {@code [V4+ Styles]}. + */ + private static Map parseStyles(ParsableByteArray data) { + Map styles = new LinkedHashMap<>(); + @Nullable SsaStyle.Format formatInfo = null; + @Nullable String currentLine; + while ((currentLine = data.readLine()) != null + && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + formatInfo = SsaStyle.Format.fromFormatLine(currentLine); + } else if (currentLine.startsWith(STYLE_LINE_PREFIX)) { + if (formatInfo == null) { + Log.w(TAG, "Skipping 'Style:' line before 'Format:' line: " + currentLine); + continue; + } + @Nullable SsaStyle style = SsaStyle.fromStyleLine(currentLine, formatInfo); + if (style != null) { + styles.put(style.name, style); + } + } + } + return styles; + } + /** * Parses the event body of the subtitle. * * @param data A {@link ParsableByteArray} from which the body should be read. * @param cues A list to which parsed cues will be added. - * @param cueTimesUs An array to which parsed cue timestamps will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ - private void parseEventBody(ParsableByteArray data, List cues, LongArray cueTimesUs) { - String currentLine; + private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + @Nullable + SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; + @Nullable String currentLine; while ((currentLine = data.readLine()) != null) { - if (!haveInitializationData && currentLine.startsWith(FORMAT_LINE_PREFIX)) { - parseFormatLine(currentLine); + if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { + format = SsaDialogueFormat.fromFormatLine(currentLine); } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { - parseDialogueLine(currentLine, cues, cueTimesUs); + if (format == null) { + Log.w(TAG, "Skipping dialogue line before complete format: " + currentLine); + continue; + } + parseDialogueLine(currentLine, format, cues, cueTimesUs); } } } - /** - * Parses a format line. - * - * @param formatLine The line to parse. - */ - private void parseFormatLine(String formatLine) { - String[] values = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); - formatKeyCount = values.length; - formatStartIndex = C.INDEX_UNSET; - formatEndIndex = C.INDEX_UNSET; - formatTextIndex = C.INDEX_UNSET; - for (int i = 0; i < formatKeyCount; i++) { - String key = Util.toLowerInvariant(values[i].trim()); - switch (key) { - case "start": - formatStartIndex = i; - break; - case "end": - formatEndIndex = i; - break; - case "text": - formatTextIndex = i; - break; - default: - // Do nothing. - break; - } - } - if (formatStartIndex == C.INDEX_UNSET - || formatEndIndex == C.INDEX_UNSET - || formatTextIndex == C.INDEX_UNSET) { - // Set to 0 so that parseDialogueLine skips lines until a complete format line is found. - formatKeyCount = 0; - } - } - /** * Parses a dialogue line. * - * @param dialogueLine The line to parse. + * @param dialogueLine The dialogue values (i.e. everything after {@code Dialogue:}). + * @param format The dialogue format to use when parsing {@code dialogueLine}. * @param cues A list to which parsed cues will be added. - * @param cueTimesUs An array to which parsed cue timestamps will be added. + * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. */ - private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { - if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); - return; - } - - String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) - .split(",", formatKeyCount); - if (lineValues.length != formatKeyCount) { + private void parseDialogueLine( + String dialogueLine, SsaDialogueFormat format, List> cues, List cueTimesUs) { + Assertions.checkArgument(dialogueLine.startsWith(DIALOGUE_LINE_PREFIX)); + String[] lineValues = + dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()).split(",", format.length); + if (lineValues.length != format.length) { Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); return; } - long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]); + long startTimeUs = parseTimecodeUs(lineValues[format.startTimeIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); return; } - long endTimeUs = C.TIME_UNSET; - String endTimeString = lineValues[formatEndIndex]; - if (!endTimeString.trim().isEmpty()) { - endTimeUs = SsaDecoder.parseTimecodeUs(endTimeString); - if (endTimeUs == C.TIME_UNSET) { - Log.w(TAG, "Skipping invalid timing: " + dialogueLine); - return; - } + long endTimeUs = parseTimecodeUs(lineValues[format.endTimeIndex]); + if (endTimeUs == C.TIME_UNSET) { + Log.w(TAG, "Skipping invalid timing: " + dialogueLine); + return; } - String text = lineValues[formatTextIndex] - .replaceAll("\\{.*?\\}", "") - .replaceAll("\\\\N", "\n") - .replaceAll("\\\\n", "\n"); - cues.add(new Cue(text)); - cueTimesUs.add(startTimeUs); - if (endTimeUs != C.TIME_UNSET) { - cues.add(Cue.EMPTY); - cueTimesUs.add(endTimeUs); + @Nullable + SsaStyle style = + styles != null && format.styleIndex != C.INDEX_UNSET + ? styles.get(lineValues[format.styleIndex].trim()) + : null; + String rawText = lineValues[format.textIndex]; + SsaStyle.Overrides styleOverrides = SsaStyle.Overrides.parseFromDialogue(rawText); + String text = + SsaStyle.Overrides.stripStyleOverrides(rawText) + .replaceAll("\\\\N", "\n") + .replaceAll("\\\\n", "\n"); + Cue cue = createCue(text, style, styleOverrides, screenWidth, screenHeight); + + int startTimeIndex = addCuePlacerholderByTime(startTimeUs, cueTimesUs, cues); + int endTimeIndex = addCuePlacerholderByTime(endTimeUs, cueTimesUs, cues); + // Iterate on cues from startTimeIndex until endTimeIndex, adding the current cue. + for (int i = startTimeIndex; i < endTimeIndex; i++) { + cues.get(i).add(cue); } } @@ -213,16 +280,167 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param timeString The string to parse. * @return The parsed timestamp in microseconds. */ - public static long parseTimecodeUs(String timeString) { - Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString); + private static long parseTimecodeUs(String timeString) { + Matcher matcher = SSA_TIMECODE_PATTERN.matcher(timeString.trim()); if (!matcher.matches()) { return C.TIME_UNSET; } - long timestampUs = Long.parseLong(matcher.group(1)) * 60 * 60 * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(2)) * 60 * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(3)) * C.MICROS_PER_SECOND; - timestampUs += Long.parseLong(matcher.group(4)) * 10000; // 100ths of a second. + long timestampUs = + Long.parseLong(castNonNull(matcher.group(1))) * 60 * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(2))) * 60 * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(3))) * C.MICROS_PER_SECOND; + timestampUs += Long.parseLong(castNonNull(matcher.group(4))) * 10000; // 100ths of a second. return timestampUs; } + private static Cue createCue( + String text, + @Nullable SsaStyle style, + SsaStyle.Overrides styleOverrides, + float screenWidth, + float screenHeight) { + @SsaStyle.SsaAlignment int alignment; + if (styleOverrides.alignment != SsaStyle.SSA_ALIGNMENT_UNKNOWN) { + alignment = styleOverrides.alignment; + } else if (style != null) { + alignment = style.alignment; + } else { + alignment = SsaStyle.SSA_ALIGNMENT_UNKNOWN; + } + @Cue.AnchorType int positionAnchor = toPositionAnchor(alignment); + @Cue.AnchorType int lineAnchor = toLineAnchor(alignment); + + float position; + float line; + if (styleOverrides.position != null + && screenHeight != Cue.DIMEN_UNSET + && screenWidth != Cue.DIMEN_UNSET) { + position = styleOverrides.position.x / screenWidth; + line = styleOverrides.position.y / screenHeight; + } else { + // TODO: Read the MarginL, MarginR and MarginV values from the Style & Dialogue lines. + position = computeDefaultLineOrPosition(positionAnchor); + line = computeDefaultLineOrPosition(lineAnchor); + } + + return new Cue( + text, + toTextAlignment(alignment), + line, + Cue.LINE_TYPE_FRACTION, + lineAnchor, + position, + positionAnchor, + /* size= */ Cue.DIMEN_UNSET); + } + + @Nullable + private static Layout.Alignment toTextAlignment(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + return Layout.Alignment.ALIGN_NORMAL; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + return Layout.Alignment.ALIGN_CENTER; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Layout.Alignment.ALIGN_OPPOSITE; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return null; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return null; + } + } + + @Cue.AnchorType + private static int toLineAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + @Cue.AnchorType + private static int toPositionAnchor(@SsaStyle.SsaAlignment int alignment) { + switch (alignment) { + case SsaStyle.SSA_ALIGNMENT_BOTTOM_LEFT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_LEFT: + case SsaStyle.SSA_ALIGNMENT_TOP_LEFT: + return Cue.ANCHOR_TYPE_START; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_CENTER: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_CENTER: + case SsaStyle.SSA_ALIGNMENT_TOP_CENTER: + return Cue.ANCHOR_TYPE_MIDDLE; + case SsaStyle.SSA_ALIGNMENT_BOTTOM_RIGHT: + case SsaStyle.SSA_ALIGNMENT_MIDDLE_RIGHT: + case SsaStyle.SSA_ALIGNMENT_TOP_RIGHT: + return Cue.ANCHOR_TYPE_END; + case SsaStyle.SSA_ALIGNMENT_UNKNOWN: + return Cue.TYPE_UNSET; + default: + Log.w(TAG, "Unknown alignment: " + alignment); + return Cue.TYPE_UNSET; + } + } + + private static float computeDefaultLineOrPosition(@Cue.AnchorType int anchor) { + switch (anchor) { + case Cue.ANCHOR_TYPE_START: + return DEFAULT_MARGIN; + case Cue.ANCHOR_TYPE_MIDDLE: + return 0.5f; + case Cue.ANCHOR_TYPE_END: + return 1.0f - DEFAULT_MARGIN; + case Cue.TYPE_UNSET: + default: + return Cue.DIMEN_UNSET; + } + } + + /** + * Searches for {@code timeUs} in {@code sortedCueTimesUs}, inserting it if it's not found, and + * returns the index. + * + *

If it's inserted, we also insert a matching entry to {@code cues}. + */ + private static int addCuePlacerholderByTime( + long timeUs, List sortedCueTimesUs, List> cues) { + int insertionIndex = 0; + for (int i = sortedCueTimesUs.size() - 1; i >= 0; i--) { + if (sortedCueTimesUs.get(i) == timeUs) { + return i; + } + + if (sortedCueTimesUs.get(i) < timeUs) { + insertionIndex = i + 1; + break; + } + } + sortedCueTimesUs.add(insertionIndex, timeUs); + // Copy over cues from left, or use an empty list if we're inserting at the beginning. + cues.add( + insertionIndex, + insertionIndex == 0 ? new ArrayList<>() : new ArrayList<>(cues.get(insertionIndex - 1))); + return insertionIndex; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java new file mode 100644 index 000000000..03c025cd9 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDialogueFormat.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.android.exoplayer2.text.ssa; + +import static com.google.android.exoplayer2.text.ssa.SsaDecoder.FORMAT_LINE_PREFIX; + +import android.text.TextUtils; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * Represents a {@code Format:} line from the {@code [Events]} section + * + *

The indices are used to determine the location of particular properties in each {@code + * Dialogue:} line. + */ +/* package */ final class SsaDialogueFormat { + + public final int startTimeIndex; + public final int endTimeIndex; + public final int styleIndex; + public final int textIndex; + public final int length; + + private SsaDialogueFormat( + int startTimeIndex, int endTimeIndex, int styleIndex, int textIndex, int length) { + this.startTimeIndex = startTimeIndex; + this.endTimeIndex = endTimeIndex; + this.styleIndex = styleIndex; + this.textIndex = textIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [Events] section. + * + * @return the parsed info, or null if {@code formatLine} doesn't contain both 'start' and 'end'. + */ + @Nullable + public static SsaDialogueFormat fromFormatLine(String formatLine) { + int startTimeIndex = C.INDEX_UNSET; + int endTimeIndex = C.INDEX_UNSET; + int styleIndex = C.INDEX_UNSET; + int textIndex = C.INDEX_UNSET; + Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); + String[] keys = TextUtils.split(formatLine.substring(FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "start": + startTimeIndex = i; + break; + case "end": + endTimeIndex = i; + break; + case "style": + styleIndex = i; + break; + case "text": + textIndex = i; + break; + } + } + return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET) + ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length) + : null; + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java new file mode 100644 index 000000000..fd2cb036b --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaStyle.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.android.exoplayer2.text.ssa; + +import static com.google.android.exoplayer2.text.ssa.SsaDecoder.STYLE_LINE_PREFIX; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.graphics.PointF; +import android.text.TextUtils; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Represents a line from an SSA/ASS {@code [V4+ Styles]} section. */ +/* package */ final class SsaStyle { + + private static final String TAG = "SsaStyle"; + + /** + * The SSA/ASS alignments. + * + *

Allowed values: + * + *

    + *
  • {@link #SSA_ALIGNMENT_UNKNOWN} + *
  • {@link #SSA_ALIGNMENT_BOTTOM_LEFT} + *
  • {@link #SSA_ALIGNMENT_BOTTOM_CENTER} + *
  • {@link #SSA_ALIGNMENT_BOTTOM_RIGHT} + *
  • {@link #SSA_ALIGNMENT_MIDDLE_LEFT} + *
  • {@link #SSA_ALIGNMENT_MIDDLE_CENTER} + *
  • {@link #SSA_ALIGNMENT_MIDDLE_RIGHT} + *
  • {@link #SSA_ALIGNMENT_TOP_LEFT} + *
  • {@link #SSA_ALIGNMENT_TOP_CENTER} + *
  • {@link #SSA_ALIGNMENT_TOP_RIGHT} + *
+ */ + @IntDef({ + SSA_ALIGNMENT_UNKNOWN, + SSA_ALIGNMENT_BOTTOM_LEFT, + SSA_ALIGNMENT_BOTTOM_CENTER, + SSA_ALIGNMENT_BOTTOM_RIGHT, + SSA_ALIGNMENT_MIDDLE_LEFT, + SSA_ALIGNMENT_MIDDLE_CENTER, + SSA_ALIGNMENT_MIDDLE_RIGHT, + SSA_ALIGNMENT_TOP_LEFT, + SSA_ALIGNMENT_TOP_CENTER, + SSA_ALIGNMENT_TOP_RIGHT, + }) + @Documented + @Retention(SOURCE) + public @interface SsaAlignment {} + + // The numbering follows the ASS (v4+) spec (i.e. the points on the number pad). + public static final int SSA_ALIGNMENT_UNKNOWN = -1; + public static final int SSA_ALIGNMENT_BOTTOM_LEFT = 1; + public static final int SSA_ALIGNMENT_BOTTOM_CENTER = 2; + public static final int SSA_ALIGNMENT_BOTTOM_RIGHT = 3; + public static final int SSA_ALIGNMENT_MIDDLE_LEFT = 4; + public static final int SSA_ALIGNMENT_MIDDLE_CENTER = 5; + public static final int SSA_ALIGNMENT_MIDDLE_RIGHT = 6; + public static final int SSA_ALIGNMENT_TOP_LEFT = 7; + public static final int SSA_ALIGNMENT_TOP_CENTER = 8; + public static final int SSA_ALIGNMENT_TOP_RIGHT = 9; + + public final String name; + @SsaAlignment public final int alignment; + + private SsaStyle(String name, @SsaAlignment int alignment) { + this.name = name; + this.alignment = alignment; + } + + @Nullable + public static SsaStyle fromStyleLine(String styleLine, Format format) { + Assertions.checkArgument(styleLine.startsWith(STYLE_LINE_PREFIX)); + String[] styleValues = TextUtils.split(styleLine.substring(STYLE_LINE_PREFIX.length()), ","); + if (styleValues.length != format.length) { + Log.w( + TAG, + Util.formatInvariant( + "Skipping malformed 'Style:' line (expected %s values, found %s): '%s'", + format.length, styleValues.length, styleLine)); + return null; + } + try { + return new SsaStyle( + styleValues[format.nameIndex].trim(), parseAlignment(styleValues[format.alignmentIndex])); + } catch (RuntimeException e) { + Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); + return null; + } + } + + @SsaAlignment + private static int parseAlignment(String alignmentStr) { + try { + @SsaAlignment int alignment = Integer.parseInt(alignmentStr.trim()); + if (isValidAlignment(alignment)) { + return alignment; + } + } catch (NumberFormatException e) { + // Swallow the exception and return UNKNOWN below. + } + Log.w(TAG, "Ignoring unknown alignment: " + alignmentStr); + return SSA_ALIGNMENT_UNKNOWN; + } + + private static boolean isValidAlignment(@SsaAlignment int alignment) { + switch (alignment) { + case SSA_ALIGNMENT_BOTTOM_CENTER: + case SSA_ALIGNMENT_BOTTOM_LEFT: + case SSA_ALIGNMENT_BOTTOM_RIGHT: + case SSA_ALIGNMENT_MIDDLE_CENTER: + case SSA_ALIGNMENT_MIDDLE_LEFT: + case SSA_ALIGNMENT_MIDDLE_RIGHT: + case SSA_ALIGNMENT_TOP_CENTER: + case SSA_ALIGNMENT_TOP_LEFT: + case SSA_ALIGNMENT_TOP_RIGHT: + return true; + case SSA_ALIGNMENT_UNKNOWN: + default: + return false; + } + } + + /** + * Represents a {@code Format:} line from the {@code [V4+ Styles]} section + * + *

The indices are used to determine the location of particular properties in each {@code + * Style:} line. + */ + /* package */ static final class Format { + + public final int nameIndex; + public final int alignmentIndex; + public final int length; + + private Format(int nameIndex, int alignmentIndex, int length) { + this.nameIndex = nameIndex; + this.alignmentIndex = alignmentIndex; + this.length = length; + } + + /** + * Parses the format info from a 'Format:' line in the [V4+ Styles] section. + * + * @return the parsed info, or null if {@code styleFormatLine} doesn't contain 'name'. + */ + @Nullable + public static Format fromFormatLine(String styleFormatLine) { + int nameIndex = C.INDEX_UNSET; + int alignmentIndex = C.INDEX_UNSET; + String[] keys = + TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); + for (int i = 0; i < keys.length; i++) { + switch (Util.toLowerInvariant(keys[i].trim())) { + case "name": + nameIndex = i; + break; + case "alignment": + alignmentIndex = i; + break; + } + } + return nameIndex != C.INDEX_UNSET ? new Format(nameIndex, alignmentIndex, keys.length) : null; + } + } + + /** + * Represents the style override information parsed from an SSA/ASS dialogue line. + * + *

Overrides are contained in braces embedded in the dialogue text of the cue. + */ + /* package */ static final class Overrides { + + private static final String TAG = "SsaStyle.Overrides"; + + /** Matches "{foo}" and returns "foo" in group 1 */ + // Warning that \\} can be replaced with } is bogus [internal: b/144480183]. + private static final Pattern BRACES_PATTERN = Pattern.compile("\\{([^}]*)\\}"); + + private static final String PADDED_DECIMAL_PATTERN = "\\s*\\d+(?:\\.\\d+)?\\s*"; + + /** Matches "\pos(x,y)" and returns "x" in group 1 and "y" in group 2 */ + private static final Pattern POSITION_PATTERN = + Pattern.compile(Util.formatInvariant("\\\\pos\\((%1$s),(%1$s)\\)", PADDED_DECIMAL_PATTERN)); + /** Matches "\move(x1,y1,x2,y2[,t1,t2])" and returns "x2" in group 1 and "y2" in group 2 */ + private static final Pattern MOVE_PATTERN = + Pattern.compile( + Util.formatInvariant( + "\\\\move\\(%1$s,%1$s,(%1$s),(%1$s)(?:,%1$s,%1$s)?\\)", PADDED_DECIMAL_PATTERN)); + + /** Matches "\anx" and returns x in group 1 */ + private static final Pattern ALIGNMENT_OVERRIDE_PATTERN = Pattern.compile("\\\\an(\\d+)"); + + @SsaAlignment public final int alignment; + @Nullable public final PointF position; + + private Overrides(@SsaAlignment int alignment, @Nullable PointF position) { + this.alignment = alignment; + this.position = position; + } + + public static Overrides parseFromDialogue(String text) { + @SsaAlignment int alignment = SSA_ALIGNMENT_UNKNOWN; + PointF position = null; + Matcher matcher = BRACES_PATTERN.matcher(text); + while (matcher.find()) { + String braceContents = matcher.group(1); + try { + PointF parsedPosition = parsePosition(braceContents); + if (parsedPosition != null) { + position = parsedPosition; + } + } catch (RuntimeException e) { + // Ignore invalid \pos() or \move() function. + } + try { + @SsaAlignment int parsedAlignment = parseAlignmentOverride(braceContents); + if (parsedAlignment != SSA_ALIGNMENT_UNKNOWN) { + alignment = parsedAlignment; + } + } catch (RuntimeException e) { + // Ignore invalid \an alignment override. + } + } + return new Overrides(alignment, position); + } + + public static String stripStyleOverrides(String dialogueLine) { + return BRACES_PATTERN.matcher(dialogueLine).replaceAll(""); + } + + /** + * Parses the position from a style override, returns null if no position is found. + * + *

The attribute is expected to be in the form {@code \pos(x,y)} or {@code + * \move(x1,y1,x2,y2,startTime,endTime)} (startTime and endTime are optional). In the case of + * {@code \move()}, this returns {@code (x2, y2)} (i.e. the end position of the move). + * + * @param styleOverride The string to parse. + * @return The parsed position, or null if no position is found. + */ + @Nullable + private static PointF parsePosition(String styleOverride) { + Matcher positionMatcher = POSITION_PATTERN.matcher(styleOverride); + Matcher moveMatcher = MOVE_PATTERN.matcher(styleOverride); + boolean hasPosition = positionMatcher.find(); + boolean hasMove = moveMatcher.find(); + + String x; + String y; + if (hasPosition) { + if (hasMove) { + Log.i( + TAG, + "Override has both \\pos(x,y) and \\move(x1,y1,x2,y2); using \\pos values. override='" + + styleOverride + + "'"); + } + x = positionMatcher.group(1); + y = positionMatcher.group(2); + } else if (hasMove) { + x = moveMatcher.group(1); + y = moveMatcher.group(2); + } else { + return null; + } + return new PointF( + Float.parseFloat(Assertions.checkNotNull(x).trim()), + Float.parseFloat(Assertions.checkNotNull(y).trim())); + } + + @SsaAlignment + private static int parseAlignmentOverride(String braceContents) { + Matcher matcher = ALIGNMENT_OVERRIDE_PATTERN.matcher(braceContents); + return matcher.find() ? parseAlignment(matcher.group(1)) : SSA_ALIGNMENT_UNKNOWN; + } + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java index 9a3756194..4093f7974 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/SsaSubtitle.java @@ -28,14 +28,14 @@ import java.util.List; */ /* package */ final class SsaSubtitle implements Subtitle { - private final Cue[] cues; - private final long[] cueTimesUs; + private final List> cues; + private final List cueTimesUs; /** * @param cues The cues in the subtitle. * @param cueTimesUs The cue times, in microseconds. */ - public SsaSubtitle(Cue[] cues, long[] cueTimesUs) { + public SsaSubtitle(List> cues, List cueTimesUs) { this.cues = cues; this.cueTimesUs = cueTimesUs; } @@ -43,30 +43,29 @@ import java.util.List; @Override public int getNextEventTimeIndex(long timeUs) { int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); - return index < cueTimesUs.length ? index : C.INDEX_UNSET; + return index < cueTimesUs.size() ? index : C.INDEX_UNSET; } @Override public int getEventTimeCount() { - return cueTimesUs.length; + return cueTimesUs.size(); } @Override public long getEventTime(int index) { Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < cueTimesUs.length); - return cueTimesUs[index]; + Assertions.checkArgument(index < cueTimesUs.size()); + return cueTimesUs.get(index); } @Override public List getCues(long timeUs) { int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues[index] == Cue.EMPTY) { - // timeUs is earlier than the start of the first cue, or we have an empty cue. + if (index == -1) { + // timeUs is earlier than the start of the first cue. return Collections.emptyList(); } else { - return Collections.singletonList(cues[index]); + return cues.get(index); } } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java new file mode 100644 index 000000000..cdf891d01 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ssa/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.text.ssa; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 5dfaecee1..cef7e3f53 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -15,12 +15,13 @@ */ package com.google.android.exoplayer2.text.subrip; -import androidx.annotation.Nullable; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -34,16 +35,18 @@ import java.util.regex.Pattern; public final class SubripDecoder extends SimpleSubtitleDecoder { // Fractional positions for use when alignment tags are present. - /* package */ static final float START_FRACTION = 0.08f; - /* package */ static final float END_FRACTION = 1 - START_FRACTION; - /* package */ static final float MID_FRACTION = 0.5f; + private static final float START_FRACTION = 0.08f; + private static final float END_FRACTION = 1 - START_FRACTION; + private static final float MID_FRACTION = 0.5f; private static final String TAG = "SubripDecoder"; - private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"; + // Some SRT files don't include hours or milliseconds in the timecode, so we use optional groups. + private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+)(?:,(\\d+))?"; private static final Pattern SUBRIP_TIMING_LINE = - Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")?\\s*"); + Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")\\s*"); + // NOTE: Android Studio's suggestion to simplify '\\}' is incorrect [internal: b/144480183]. private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile("\\{\\\\.*?\\}"); private static final String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}"; @@ -68,12 +71,12 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } @Override - protected SubripSubtitle decode(byte[] bytes, int length, boolean reset) { + protected Subtitle decode(byte[] bytes, int length, boolean reset) { ArrayList cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); ParsableByteArray subripData = new ParsableByteArray(bytes, length); - String currentLine; + @Nullable String currentLine; while ((currentLine = subripData.readLine()) != null) { if (currentLine.length() == 0) { // Skip blank lines. @@ -89,7 +92,6 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } // Read and parse the timing line. - boolean haveEndTimecode = false; currentLine = subripData.readLine(); if (currentLine == null) { Log.w(TAG, "Unexpected end"); @@ -98,11 +100,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); if (matcher.matches()) { - cueTimesUs.add(parseTimecode(matcher, 1)); - if (!TextUtils.isEmpty(matcher.group(6))) { - haveEndTimecode = true; - cueTimesUs.add(parseTimecode(matcher, 6)); - } + cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 1)); + cueTimesUs.add(parseTimecode(matcher, /* groupOffset= */ 6)); } else { Log.w(TAG, "Skipping invalid timing: " + currentLine); continue; @@ -122,7 +121,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { Spanned text = Html.fromHtml(textBuilder.toString()); - String alignmentTag = null; + @Nullable String alignmentTag = null; for (int i = 0; i < tags.size(); i++) { String tag = tags.get(i); if (tag.matches(SUBRIP_ALIGNMENT_TAG)) { @@ -132,10 +131,7 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } } cues.add(buildCue(text, alignmentTag)); - - if (haveEndTimecode) { - cues.add(Cue.EMPTY); - } + cues.add(Cue.EMPTY); } Cue[] cuesArray = new Cue[cues.size()]; @@ -235,10 +231,14 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } private static long parseTimecode(Matcher matcher, int groupOffset) { - long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000; + @Nullable String hours = matcher.group(groupOffset + 1); + long timestampMs = hours != null ? Long.parseLong(hours) * 60 * 60 * 1000 : 0; timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000; timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000; - timestampMs += Long.parseLong(matcher.group(groupOffset + 4)); + @Nullable String millis = matcher.group(groupOffset + 4); + if (millis != null) { + timestampMs += Long.parseLong(millis); + } return timestampMs * 1000; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/subrip/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/subrip/package-info.java new file mode 100644 index 000000000..bb7565c07 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/subrip/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.text.subrip; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index 6e0c49546..6dabcdd90 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -19,6 +19,7 @@ import android.text.Layout; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.Log; @@ -102,7 +103,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } @Override - protected TtmlSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java index 3b4d061aa..3365749e1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlNode.java @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.text.ttml; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import androidx.annotation.Nullable; import android.text.SpannableStringBuilder; import android.util.Base64; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Assertions; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java index 9fdcc48c1..e90b09917 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlStyle.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.text.ttml; import android.graphics.Typeface; -import androidx.annotation.IntDef; import android.text.Layout; +import androidx.annotation.IntDef; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java new file mode 100644 index 000000000..5b0685e24 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/ttml/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.text.ttml; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index ddc7a8f5f..c8f2979c5 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -43,8 +43,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { private static final char BOM_UTF16_BE = '\uFEFF'; private static final char BOM_UTF16_LE = '\uFFFE'; - private static final int TYPE_STYL = Util.getIntegerCodeForString("styl"); - private static final int TYPE_TBOX = Util.getIntegerCodeForString("tbox"); + private static final int TYPE_STYL = 0x7374796c; + private static final int TYPE_TBOX = 0x74626f78; private static final String TX3G_SERIF = "Serif"; private static final int SIZE_ATOM_HEADER = 8; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/tx3g/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/tx3g/package-info.java new file mode 100644 index 000000000..2ae99adf5 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/tx3g/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.text.tx3g; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index f87710a44..9a5ac40a0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -16,10 +16,10 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -import java.util.Arrays; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -98,13 +98,14 @@ import java.util.regex.Pattern; } /** - * Returns a string containing the selector. The input is expected to have the form - * {@code ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. + * Returns a string containing the selector. The input is expected to have the form {@code + * ::cue(tag#id.class1.class2[voice="someone"]}, where every element is optional. * * @param input From which the selector is obtained. - * @return A string containing the target, empty string if the selector is universal - * (targets all cues) or null if an error was encountered. + * @return A string containing the target, empty string if the selector is universal (targets all + * cues) or null if an error was encountered. */ + @Nullable private static String parseSelector(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() < 5) { @@ -128,7 +129,7 @@ import java.util.regex.Pattern; target = readCueTarget(input); } token = parseNextToken(input, stringBuilder); - if (!")".equals(token) || token == null) { + if (!")".equals(token)) { return null; } return target; @@ -208,6 +209,7 @@ import java.util.regex.Pattern; } // Visible for testing. + @Nullable /* package */ static String parseNextToken(ParsableByteArray input, StringBuilder stringBuilder) { skipWhitespaceAndComments(input); if (input.bytesLeft() == 0) { @@ -249,6 +251,7 @@ import java.util.regex.Pattern; return (char) input.data[position]; } + @Nullable private static String parsePropertyValue(ParsableByteArray input, StringBuilder stringBuilder) { StringBuilder expressionBuilder = new StringBuilder(); String token; @@ -337,7 +340,7 @@ import java.util.regex.Pattern; style.setTargetTagName(tagAndIdDivision); } if (classDivision.length > 1) { - style.setTargetClasses(Arrays.copyOfRange(classDivision, 1, classDivision.length)); + style.setTargetClasses(Util.nullSafeArrayCopyOfRange(classDivision, 1, classDivision.length)); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 8cb0ac58c..8b255ac2b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text.webvtt; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -24,16 +25,20 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -/** - * A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. - */ +/** A {@link SimpleSubtitleDecoder} for Webvtt embedded in a Mp4 container file. */ +@SuppressWarnings("ConstantField") public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { 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"); + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_payl = 0x7061796c; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_sttg = 0x73747467; + + @SuppressWarnings("ConstantCaseForConstants") + private static final int TYPE_vttc = 0x76747463; private final ParsableByteArray sampleData; private final WebvttCue.Builder builder; @@ -45,7 +50,7 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected Mp4WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { // 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. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java index ded7ef73f..97c0acb1e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java @@ -16,8 +16,10 @@ package com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; -import androidx.annotation.IntDef; import android.text.Layout; +import android.text.TextUtils; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -25,6 +27,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; /** * Style object of a Css style block in a Webvtt file. @@ -80,7 +83,7 @@ public final class WebvttCssStyle { private String targetVoice; // Style properties. - private String fontFamily; + @Nullable private String fontFamily; private int fontColor; private boolean hasFontColor; private int backgroundColor; @@ -91,12 +94,16 @@ public final class WebvttCssStyle { @OptionalBoolean private int italic; @FontSizeUnit private int fontSizeUnit; private float fontSize; - private Layout.Alignment textAlign; + @Nullable private Layout.Alignment textAlign; + // Calling reset() is forbidden because `this` isn't initialized. This can be safely suppressed + // because reset() only assigns fields, it doesn't read any. + @SuppressWarnings("nullness:method.invocation.invalid") public WebvttCssStyle() { reset(); } + @EnsuresNonNull({"targetId", "targetTag", "targetClasses", "targetVoice"}) public void reset() { targetId = ""; targetTag = ""; @@ -133,14 +140,13 @@ public final class WebvttCssStyle { * Returns a value in a score system compliant with the CSS Specificity rules. * * @see CSS Cascading - * - * The score works as follows: - *

    - *
  • Id match adds 0x40000000 to the score. - *
  • Each class and voice match adds 4 to the score. - *
  • Tag matching adds 2 to the score. - *
  • Universal selector matching scores 1. - *
+ *

The score works as follows: + *

    + *
  • Id match adds 0x40000000 to the score. + *
  • Each class and voice match adds 4 to the score. + *
  • Tag matching adds 2 to the score. + *
  • Universal selector matching scores 1. + *
* * @param id The id of the cue if present, {@code null} otherwise. * @param tag Name of the tag, {@code null} if it refers to the entire cue. @@ -148,12 +154,13 @@ public final class WebvttCssStyle { * @param voice Annotated voice if present, {@code null} otherwise. * @return The score of the match, zero if there is no match. */ - public int getSpecificityScore(String id, String tag, String[] classes, String voice) { + public int getSpecificityScore( + @Nullable String id, @Nullable String tag, String[] classes, @Nullable String voice) { if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty() && targetVoice.isEmpty()) { // The selector is universal. It matches with the minimum score if and only if the given // element is a whole cue. - return tag.isEmpty() ? 1 : 0; + return TextUtils.isEmpty(tag) ? 1 : 0; } int score = 0; score = updateScoreForMatch(score, targetId, id, 0x40000000); @@ -208,11 +215,12 @@ public final class WebvttCssStyle { return this; } + @Nullable public String getFontFamily() { return fontFamily; } - public WebvttCssStyle setFontFamily(String fontFamily) { + public WebvttCssStyle setFontFamily(@Nullable String fontFamily) { this.fontFamily = Util.toLowerInvariant(fontFamily); return this; } @@ -251,11 +259,12 @@ public final class WebvttCssStyle { return hasBackgroundColor; } + @Nullable public Layout.Alignment getTextAlign() { return textAlign; } - public WebvttCssStyle setTextAlign(Layout.Alignment textAlign) { + public WebvttCssStyle setTextAlign(@Nullable Layout.Alignment textAlign) { this.textAlign = textAlign; return this; } @@ -309,8 +318,8 @@ public final class WebvttCssStyle { } } - private static int updateScoreForMatch(int currentScore, String target, String actual, - int score) { + private static int updateScoreForMatch( + int currentScore, String target, @Nullable String actual, int score) { if (target.isEmpty() || currentScore == -1) { return currentScore; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java index 857b1562e..55e568efa 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCue.java @@ -15,31 +15,36 @@ */ package com.google.android.exoplayer2.text.webvtt; -import android.text.Layout.Alignment; -import android.text.SpannableStringBuilder; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.util.Log; +import static java.lang.annotation.RetentionPolicy.SOURCE; -/** - * A representation of a WebVTT cue. - */ +import android.text.Layout.Alignment; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +/** A representation of a WebVTT cue. */ public final class WebvttCue extends Cue { + private static final float DEFAULT_POSITION = 0.5f; + 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, @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float position, - @Cue.AnchorType int positionAnchor, float width) { + private WebvttCue( + long startTime, + long endTime, + CharSequence text, + @Nullable Alignment textAlignment, + float line, + @Cue.LineType int lineType, + @Cue.AnchorType int lineAnchor, + float position, + @Cue.AnchorType int positionAnchor, + float width) { super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); this.startTime = startTime; this.endTime = endTime; @@ -52,30 +57,81 @@ public final class WebvttCue extends Cue { * @return Whether this cue should be placed in the default position. */ public boolean isNormalCue() { - return (line == DIMEN_UNSET && position == DIMEN_UNSET); + return (line == DIMEN_UNSET && position == DEFAULT_POSITION); } - /** - * Builder for WebVTT cues. - */ + /** Builder for WebVTT cues. */ @SuppressWarnings("hiding") public static class Builder { + /** + * Valid values for {@link #setTextAlignment(int)}. + * + *

We use a custom list (and not {@link Alignment} directly) in order to include both {@code + * START}/{@code LEFT} and {@code END}/{@code RIGHT}. The distinction is important for {@link + * #derivePosition(int)}. + * + *

These correspond to the valid values for the 'align' cue setting in the WebVTT spec. + */ + @Documented + @Retention(SOURCE) + @IntDef({ + TEXT_ALIGNMENT_START, + TEXT_ALIGNMENT_CENTER, + TEXT_ALIGNMENT_END, + TEXT_ALIGNMENT_LEFT, + TEXT_ALIGNMENT_RIGHT + }) + public @interface TextAlignment {} + /** + * See WebVTT's align:start. + */ + public static final int TEXT_ALIGNMENT_START = 1; + + /** + * See WebVTT's align:center. + */ + public static final int TEXT_ALIGNMENT_CENTER = 2; + + /** + * See WebVTT's align:end. + */ + public static final int TEXT_ALIGNMENT_END = 3; + + /** + * See WebVTT's align:left. + */ + public static final int TEXT_ALIGNMENT_LEFT = 4; + + /** + * See WebVTT's align:right. + */ + public static final int TEXT_ALIGNMENT_RIGHT = 5; + private static final String TAG = "WebvttCueBuilder"; private long startTime; private long endTime; - private SpannableStringBuilder text; - private Alignment textAlignment; + @Nullable private CharSequence text; + @TextAlignment private int textAlignment; private float line; - private int lineType; - private int lineAnchor; + // Equivalent to WebVTT's snap-to-lines flag: + // https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag + @LineType private int lineType; + @AnchorType private int lineAnchor; private float position; - private int positionAnchor; + @AnchorType private int positionAnchor; private float width; // Initialization methods + // Calling reset() is forbidden because `this` isn't initialized. This can be safely + // suppressed because reset() only assigns fields, it doesn't read any. + @SuppressWarnings("nullness:method.invocation.invalid") public Builder() { reset(); } @@ -84,23 +140,45 @@ public final class WebvttCue extends Cue { startTime = 0; endTime = 0; text = null; - textAlignment = null; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment + textAlignment = TEXT_ALIGNMENT_CENTER; line = Cue.DIMEN_UNSET; - lineType = Cue.TYPE_UNSET; - lineAnchor = Cue.TYPE_UNSET; + // Defaults to NUMBER (true): https://www.w3.org/TR/webvtt1/#webvtt-cue-snap-to-lines-flag + lineType = Cue.LINE_TYPE_NUMBER; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-line-alignment + lineAnchor = Cue.ANCHOR_TYPE_START; position = Cue.DIMEN_UNSET; positionAnchor = Cue.TYPE_UNSET; - width = Cue.DIMEN_UNSET; + // Default: https://www.w3.org/TR/webvtt1/#webvtt-cue-size + width = 1.0f; } // Construction methods. public WebvttCue build() { - if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) { - derivePositionAnchorFromAlignment(); + line = computeLine(line, lineType); + + if (position == Cue.DIMEN_UNSET) { + position = derivePosition(textAlignment); } - return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor, - position, positionAnchor, width); + + if (positionAnchor == Cue.TYPE_UNSET) { + positionAnchor = derivePositionAnchor(textAlignment); + } + + width = Math.min(width, deriveMaxSize(positionAnchor, position)); + + return new WebvttCue( + startTime, + endTime, + Assertions.checkNotNull(text), + convertTextAlignment(textAlignment), + line, + lineType, + lineAnchor, + position, + positionAnchor, + width); } public Builder setStartTime(long time) { @@ -113,12 +191,12 @@ public final class WebvttCue extends Cue { return this; } - public Builder setText(SpannableStringBuilder aText) { - text = aText; + public Builder setText(CharSequence text) { + this.text = text; return this; } - public Builder setTextAlignment(Alignment textAlignment) { + public Builder setTextAlignment(@TextAlignment int textAlignment) { this.textAlignment = textAlignment; return this; } @@ -128,12 +206,12 @@ public final class WebvttCue extends Cue { return this; } - public Builder setLineType(int lineType) { + public Builder setLineType(@LineType int lineType) { this.lineType = lineType; return this; } - public Builder setLineAnchor(int lineAnchor) { + public Builder setLineAnchor(@AnchorType int lineAnchor) { this.lineAnchor = lineAnchor; return this; } @@ -143,7 +221,7 @@ public final class WebvttCue extends Cue { return this; } - public Builder setPositionAnchor(int positionAnchor) { + public Builder setPositionAnchor(@AnchorType int positionAnchor) { this.positionAnchor = positionAnchor; return this; } @@ -153,29 +231,89 @@ public final class WebvttCue extends Cue { return this; } - private Builder derivePositionAnchorFromAlignment() { - if (textAlignment == null) { - positionAnchor = Cue.TYPE_UNSET; + // https://www.w3.org/TR/webvtt1/#webvtt-cue-line + private static float computeLine(float line, @LineType int lineType) { + if (line != Cue.DIMEN_UNSET + && lineType == Cue.LINE_TYPE_FRACTION + && (line < 0.0f || line > 1.0f)) { + return 1.0f; // Step 1 + } else if (line != Cue.DIMEN_UNSET) { + // Step 2: Do nothing, line is already correct. + return line; + } else if (lineType == Cue.LINE_TYPE_FRACTION) { + return 1.0f; // Step 3 } 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; - } + // Steps 4 - 10 (stacking multiple simultaneous cues) are handled by WebvttSubtitle#getCues + // and WebvttCue#isNormalCue. + return DIMEN_UNSET; } - return this; } - } + // https://www.w3.org/TR/webvtt1/#webvtt-cue-position + private static float derivePosition(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_LEFT: + return 0.0f; + case TEXT_ALIGNMENT_RIGHT: + return 1.0f; + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_CENTER: + case TEXT_ALIGNMENT_END: + default: + return DEFAULT_POSITION; + } + } + // https://www.w3.org/TR/webvtt1/#webvtt-cue-position-alignment + @AnchorType + private static int derivePositionAnchor(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_LEFT: + case TEXT_ALIGNMENT_START: + return Cue.ANCHOR_TYPE_START; + case TEXT_ALIGNMENT_RIGHT: + case TEXT_ALIGNMENT_END: + return Cue.ANCHOR_TYPE_END; + case TEXT_ALIGNMENT_CENTER: + default: + return Cue.ANCHOR_TYPE_MIDDLE; + } + } + + @Nullable + private static Alignment convertTextAlignment(@TextAlignment int textAlignment) { + switch (textAlignment) { + case TEXT_ALIGNMENT_START: + case TEXT_ALIGNMENT_LEFT: + return Alignment.ALIGN_NORMAL; + case TEXT_ALIGNMENT_CENTER: + return Alignment.ALIGN_CENTER; + case TEXT_ALIGNMENT_END: + case TEXT_ALIGNMENT_RIGHT: + return Alignment.ALIGN_OPPOSITE; + default: + Log.w(TAG, "Unknown textAlignment: " + textAlignment); + return null; + } + } + + // Step 2 here: https://www.w3.org/TR/webvtt1/#processing-cue-settings + private static float deriveMaxSize(@AnchorType int positionAnchor, float position) { + switch (positionAnchor) { + case Cue.ANCHOR_TYPE_START: + return 1.0f - position; + case Cue.ANCHOR_TYPE_END: + return position; + case Cue.ANCHOR_TYPE_MIDDLE: + if (position <= 0.5f) { + return position * 2; + } else { + return (1.0f - position) * 2; + } + case Cue.TYPE_UNSET: + default: + throw new IllegalStateException(String.valueOf(positionAnchor)); + } + } + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 2361c9729..b6ddf89dc 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -16,8 +16,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.graphics.Typeface; -import androidx.annotation.NonNull; -import android.text.Layout.Alignment; +import android.text.Layout; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -31,21 +30,21 @@ import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.text.style.UnderlineSpan; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) - */ +/** Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern @@ -87,13 +86,13 @@ public final class WebvttCueParser { * 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. - * @param styles List of styles defined by the CSS style blocks preceeding the cues. + * @param builder Builder for WebVTT Cues (output parameter). + * @param styles List of styles defined by the CSS style blocks preceding the cues. * @return Whether a valid Cue was found. */ - public boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, - List styles) { - String firstLine = webvttData.readLine(); + public boolean parseCue( + ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { + @Nullable String firstLine = webvttData.readLine(); if (firstLine == null) { return false; } @@ -103,7 +102,7 @@ public final class WebvttCueParser { return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); } // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); + @Nullable String secondLine = webvttData.readLine(); if (secondLine == null) { return false; } @@ -152,11 +151,11 @@ public final class WebvttCueParser { * * @param id Id of the cue, {@code null} if it is not present. * @param markup The markup text to be parsed. - * @param styles List of styles defined by the CSS style blocks preceeding the cues. + * @param styles List of styles defined by the CSS style blocks preceding the cues. * @param builder Output builder. */ - /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder, - List styles) { + /* package */ static void parseCueText( + @Nullable String id, String markup, WebvttCue.Builder builder, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); @@ -175,8 +174,11 @@ public final class WebvttCueParser { boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH; String fullTagExpression = markup.substring(ltPos + (isClosingTag ? 2 : 1), isVoidTag ? pos - 2 : pos - 1); + if (fullTagExpression.trim().isEmpty()) { + continue; + } String tagName = getTagName(fullTagExpression); - if (tagName == null || !isSupportedTag(tagName)) { + if (!isSupportedTag(tagName)) { continue; } if (isClosingTag) { @@ -224,8 +226,13 @@ public final class WebvttCueParser { builder.setText(spannedText); } - private static boolean parseCue(String id, Matcher cueHeaderMatcher, ParsableByteArray webvttData, - WebvttCue.Builder builder, StringBuilder textBuilder, List styles) { + private static boolean parseCue( + @Nullable String id, + Matcher cueHeaderMatcher, + ParsableByteArray webvttData, + WebvttCue.Builder builder, + StringBuilder textBuilder, + List styles) { try { // Parse the cue start and end times. builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1))) @@ -239,8 +246,9 @@ public final class WebvttCueParser { // Parse the cue text. textBuilder.setLength(0); - String line; - while (!TextUtils.isEmpty(line = webvttData.readLine())) { + for (String line = webvttData.readLine(); + !TextUtils.isEmpty(line); + line = webvttData.readLine()) { if (textBuilder.length() > 0) { textBuilder.append("\n"); } @@ -252,14 +260,11 @@ public final class WebvttCueParser { // Internal methods - private static void parseLineAttribute(String s, WebvttCue.Builder builder) - throws NumberFormatException { + private static void parseLineAttribute(String s, WebvttCue.Builder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); s = s.substring(0, commaIndex); - } else { - builder.setLineAnchor(Cue.TYPE_UNSET); } if (s.endsWith("%")) { builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); @@ -274,18 +279,16 @@ public final class WebvttCueParser { } } - private static void parsePositionAttribute(String s, WebvttCue.Builder builder) - throws NumberFormatException { + private static void parsePositionAttribute(String s, WebvttCue.Builder builder) { int commaIndex = s.indexOf(','); if (commaIndex != -1) { builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1))); s = s.substring(0, commaIndex); - } else { - builder.setPositionAnchor(Cue.TYPE_UNSET); } builder.setPosition(WebvttParserUtil.parsePercentage(s)); } + @Cue.AnchorType private static int parsePositionAnchor(String s) { switch (s) { case "start": @@ -301,20 +304,24 @@ public final class WebvttCueParser { } } - private static Alignment parseTextAlignment(String s) { + @WebvttCue.Builder.TextAlignment + private static int parseTextAlignment(String s) { switch (s) { case "start": + return WebvttCue.Builder.TEXT_ALIGNMENT_START; case "left": - return Alignment.ALIGN_NORMAL; + return WebvttCue.Builder.TEXT_ALIGNMENT_LEFT; case "center": case "middle": - return Alignment.ALIGN_CENTER; + return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER; case "end": + return WebvttCue.Builder.TEXT_ALIGNMENT_END; case "right": - return Alignment.ALIGN_OPPOSITE; + return WebvttCue.Builder.TEXT_ALIGNMENT_RIGHT; default: Log.w(TAG, "Invalid alignment value: " + s); - return null; + // Default value: https://www.w3.org/TR/webvtt1/#webvtt-cue-text-alignment + return WebvttCue.Builder.TEXT_ALIGNMENT_CENTER; } } @@ -364,8 +371,12 @@ public final class WebvttCueParser { } } - private static void applySpansForTag(String cueId, StartTag startTag, SpannableStringBuilder text, - List styles, List scratchStyleMatches) { + private static void applySpansForTag( + @Nullable String cueId, + StartTag startTag, + SpannableStringBuilder text, + List styles, + List scratchStyleMatches) { int start = startTag.position; int end = text.length(); switch(startTag.name) { @@ -423,9 +434,10 @@ public final class WebvttCueParser { spannedText.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - if (style.getTextAlign() != null) { - spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + Layout.Alignment textAlign = style.getTextAlign(); + if (textAlign != null) { + spannedText.setSpan( + new AlignmentSpan.Standard(textAlign), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } switch (style.getFontSizeUnit()) { case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL: @@ -454,14 +466,15 @@ public final class WebvttCueParser { */ private static String getTagName(String tagExpression) { tagExpression = tagExpression.trim(); - if (tagExpression.isEmpty()) { - return null; - } + Assertions.checkArgument(!tagExpression.isEmpty()); return Util.splitAtFirst(tagExpression, "[ \\.]")[0]; } - private static void getApplicableStyles(List declaredStyles, String id, - StartTag tag, List output) { + private static void getApplicableStyles( + List declaredStyles, + @Nullable String id, + StartTag tag, + List output) { int styleCount = declaredStyles.size(); for (int i = 0; i < styleCount; i++) { WebvttCssStyle style = declaredStyles.get(i); @@ -508,9 +521,7 @@ public final class WebvttCueParser { public static StartTag buildStartTag(String fullTagExpression, int position) { fullTagExpression = fullTagExpression.trim(); - if (fullTagExpression.isEmpty()) { - return null; - } + Assertions.checkArgument(!fullTagExpression.isEmpty()); int voiceStartIndex = fullTagExpression.indexOf(" "); String voice; if (voiceStartIndex == -1) { @@ -523,7 +534,7 @@ public final class WebvttCueParser { String name = nameAndClasses[0]; String[] classes; if (nameAndClasses.length > 1) { - classes = Arrays.copyOfRange(nameAndClasses, 1, nameAndClasses.length); + classes = Util.nullSafeArrayCopyOfRange(nameAndClasses, 1, nameAndClasses.length); } else { classes = NO_CLASSES; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java index fe3c86bd1..9b356f098 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; +import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; @@ -55,7 +56,7 @@ public final class WebvttDecoder extends SimpleSubtitleDecoder { } @Override - protected WebvttSubtitle decode(byte[] bytes, int length, boolean reset) + protected Subtitle decode(byte[] bytes, int length, boolean reset) throws SubtitleDecoderException { parsableWebvttData.reset(bytes, length); // Initialization for consistent starting state. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java index 22aee60a9..907508311 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.webvtt; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -26,7 +27,7 @@ import java.util.regex.Pattern; */ public final class WebvttParserUtil { - private static final Pattern COMMENT = Pattern.compile("^NOTE((\u0020|\u0009).*)?$"); + private static final Pattern COMMENT = Pattern.compile("^NOTE([ \t].*)?$"); private static final String WEBVTT_HEADER = "WEBVTT"; private WebvttParserUtil() {} @@ -51,7 +52,7 @@ public final class WebvttParserUtil { * @param input The input from which the line should be read. */ public static boolean isWebvttHeaderLine(ParsableByteArray input) { - String line = input.readLine(); + @Nullable String line = input.readLine(); return line != null && line.startsWith(WEBVTT_HEADER); } @@ -98,8 +99,9 @@ public final class WebvttParserUtil { * 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. */ + @Nullable public static Matcher findNextCueHeader(ParsableByteArray input) { - String line; + @Nullable String line; while ((line = input.readLine()) != null) { if (COMMENT.matcher(line).matches()) { // Skip until the end of the comment block. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java index 1dd8000ca..2833ff2d0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -73,16 +72,16 @@ import java.util.List; @Override public List getCues(long timeUs) { - ArrayList list = null; + List list = new ArrayList<>(); 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); + // TODO(ibaker): Replace this with a closer implementation of the WebVTT spec (keeping + // individual cues, but tweaking their `line` value): + // https://www.w3.org/TR/webvtt1/#cue-computed-line 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 @@ -91,9 +90,12 @@ import java.util.List; firstNormalCue = cue; } else if (normalCueTextBuilder == null) { normalCueTextBuilder = new SpannableStringBuilder(); - normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text); + normalCueTextBuilder + .append(Assertions.checkNotNull(firstNormalCue.text)) + .append("\n") + .append(Assertions.checkNotNull(cue.text)); } else { - normalCueTextBuilder.append("\n").append(cue.text); + normalCueTextBuilder.append("\n").append(Assertions.checkNotNull(cue.text)); } } else { list.add(cue); @@ -102,17 +104,12 @@ import java.util.List; } if (normalCueTextBuilder != null) { // there were multiple normal cues, so create a new cue with all of the text - list.add(new WebvttCue(normalCueTextBuilder)); + list.add(new WebvttCue.Builder().setText(normalCueTextBuilder).build()); } 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(); - } + return list; } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/package-info.java new file mode 100644 index 000000000..ee429b526 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/text/webvtt/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +@NonNullApi +package com.google.android.exoplayer2.text.webvtt; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index a5c08d539..3e8cdd1ca 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -17,8 +17,8 @@ package com.google.android.exoplayer2.trackselection; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; @@ -39,7 +39,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** Factory for {@link AdaptiveTrackSelection} instances. */ public static class Factory implements TrackSelection.Factory { - private final @Nullable BandwidthMeter bandwidthMeter; + @Nullable private final BandwidthMeter bandwidthMeter; private final int minDurationForQualityIncreaseMs; private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; @@ -48,9 +48,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final long minTimeBetweenBufferReevaluationMs; private final Clock clock; - private TrackBitrateEstimator trackBitrateEstimator; - private boolean blockFixedTrackSelectionBandwidth; - /** Creates an adaptive track selection factory with default parameters. */ public Factory() { this( @@ -65,7 +62,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * @deprecated Use {@link #Factory()} instead. Custom bandwidth meter should be directly passed - * to the player in {@link ExoPlayerFactory}. + * to the player in {@link SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -113,7 +110,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * @deprecated Use {@link #Factory(int, int, int, float)} instead. Custom bandwidth meter should - * be directly passed to the player in {@link ExoPlayerFactory}. + * be directly passed to the player in {@link SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -182,7 +179,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * @deprecated Use {@link #Factory(int, int, int, float, float, long, Clock)} instead. Custom - * bandwidth meter should be directly passed to the player in {@link ExoPlayerFactory}. + * bandwidth meter should be directly passed to the player in {@link + * SimpleExoPlayer.Builder}. */ @Deprecated public Factory( @@ -203,28 +201,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { bufferedFractionToLiveEdgeForQualityIncrease; this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; this.clock = clock; - trackBitrateEstimator = TrackBitrateEstimator.DEFAULT; - } - - /** - * Sets a TrackBitrateEstimator. - * - *

This method is experimental, and will be renamed or removed in a future release. - * - * @param trackBitrateEstimator A {@link TrackBitrateEstimator}. - */ - public final void experimental_setTrackBitrateEstimator( - TrackBitrateEstimator trackBitrateEstimator) { - this.trackBitrateEstimator = trackBitrateEstimator; - } - - /** - * Enables blocking of the total fixed track selection bandwidth. - * - *

This method is experimental, and will be renamed or removed in a future release. - */ - public final void experimental_enableBlockFixedTrackSelectionBandwidth() { - this.blockFixedTrackSelectionBandwidth = true; } @Override @@ -234,20 +210,11 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { bandwidthMeter = this.bandwidthMeter; } TrackSelection[] selections = new TrackSelection[definitions.length]; - List adaptiveSelections = new ArrayList<>(); int totalFixedBandwidth = 0; for (int i = 0; i < definitions.length; i++) { Definition definition = definitions[i]; - if (definition == null) { - continue; - } - if (definition.tracks.length > 1) { - AdaptiveTrackSelection adaptiveSelection = - createAdaptiveTrackSelection(definition.group, bandwidthMeter, definition.tracks); - adaptiveSelection.experimental_setTrackBitrateEstimator(trackBitrateEstimator); - adaptiveSelections.add(adaptiveSelection); - selections[i] = adaptiveSelection; - } else { + if (definition != null && definition.tracks.length == 1) { + // Make fixed selections first to know their total bandwidth. selections[i] = new FixedTrackSelection( definition.group, definition.tracks[0], definition.reason, definition.data); @@ -257,9 +224,15 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } } } - if (blockFixedTrackSelectionBandwidth) { - for (int i = 0; i < adaptiveSelections.size(); i++) { - adaptiveSelections.get(i).experimental_setNonAllocatableBandwidth(totalFixedBandwidth); + List adaptiveSelections = new ArrayList<>(); + for (int i = 0; i < definitions.length; i++) { + Definition definition = definitions[i]; + if (definition != null && definition.tracks.length > 1) { + AdaptiveTrackSelection adaptiveSelection = + createAdaptiveTrackSelection( + definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth); + adaptiveSelections.add(adaptiveSelection); + selections[i] = adaptiveSelection; } } if (adaptiveSelections.size() > 1) { @@ -288,14 +261,19 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param group The {@link TrackGroup}. * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * @param tracks The indices of the selected tracks in the track group. + * @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits + * per second. * @return An {@link AdaptiveTrackSelection} for the specified tracks. */ protected AdaptiveTrackSelection createAdaptiveTrackSelection( - TrackGroup group, BandwidthMeter bandwidthMeter, int[] tracks) { + TrackGroup group, + BandwidthMeter bandwidthMeter, + int[] tracks, + int totalFixedTrackBandwidth) { return new AdaptiveTrackSelection( group, tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction), + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth), minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -319,11 +297,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final float bufferedFractionToLiveEdgeForQualityIncrease; private final long minTimeBetweenBufferReevaluationMs; private final Clock clock; - private final Format[] formats; - private final int[] formatBitrates; - private final int[] trackBitrates; - private TrackBitrateEstimator trackBitrateEstimator; private float playbackSpeed; private int selectedIndex; private int reason; @@ -341,6 +315,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { group, tracks, bandwidthMeter, + /* reservedBandwidth= */ 0, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, @@ -355,6 +330,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be * empty. May be in any order. * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param reservedBandwidth The reserved bandwidth, which shouldn't be considered available for + * use, in bits per second. * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the * selected track to switch to one of higher quality. * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the @@ -381,6 +358,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, + long reservedBandwidth, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, @@ -391,7 +369,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this( group, tracks, - new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction), + new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth), minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, @@ -422,39 +400,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { playbackSpeed = 1f; reason = C.SELECTION_REASON_UNKNOWN; lastBufferEvaluationMs = C.TIME_UNSET; - trackBitrateEstimator = TrackBitrateEstimator.DEFAULT; - formats = new Format[length]; - formatBitrates = new int[length]; - trackBitrates = new int[length]; - for (int i = 0; i < length; i++) { - @SuppressWarnings("nullness:method.invocation.invalid") - Format format = getFormat(i); - formats[i] = format; - formatBitrates[i] = formats[i].bitrate; - } - } - - /** - * Sets a TrackBitrateEstimator. - * - *

This method is experimental, and will be renamed or removed in a future release. - * - * @param trackBitrateEstimator A {@link TrackBitrateEstimator}. - */ - public void experimental_setTrackBitrateEstimator(TrackBitrateEstimator trackBitrateEstimator) { - this.trackBitrateEstimator = trackBitrateEstimator; - } - - /** - * Sets the non-allocatable bandwidth, which shouldn't be considered available. - * - *

This method is experimental, and will be renamed or removed in a future release. - * - * @param nonAllocatableBandwidth The non-allocatable bandwidth in bits per second. - */ - public void experimental_setNonAllocatableBandwidth(long nonAllocatableBandwidth) { - ((DefaultBandwidthProvider) bandwidthProvider) - .experimental_setNonAllocatableBandwidth(nonAllocatableBandwidth); } /** @@ -487,19 +432,16 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { MediaChunkIterator[] mediaChunkIterators) { long nowMs = clock.elapsedRealtime(); - // Update the estimated track bitrates. - trackBitrateEstimator.getBitrates(formats, queue, mediaChunkIterators, trackBitrates); - // Make initial selection if (reason == C.SELECTION_REASON_UNKNOWN) { reason = C.SELECTION_REASON_INITIAL; - selectedIndex = determineIdealSelectedIndex(nowMs, trackBitrates); + selectedIndex = determineIdealSelectedIndex(nowMs); return; } // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; - selectedIndex = determineIdealSelectedIndex(nowMs, trackBitrates); + selectedIndex = determineIdealSelectedIndex(nowMs); if (selectedIndex == currentSelectedIndex) { return; } @@ -537,7 +479,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public Object getSelectionData() { return null; } @@ -562,7 +505,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) { return queueSize; } - int idealSelectedIndex = determineIdealSelectedIndex(nowMs, formatBitrates); + int idealSelectedIndex = determineIdealSelectedIndex(nowMs); Format idealFormat = getFormat(idealSelectedIndex); // If the chunks contain video, discard from the first SD chunk beyond // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal @@ -627,16 +570,14 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link * Long#MIN_VALUE} to ignore blacklisting. - * @param trackBitrates The estimated track bitrates. May differ from format bitrates if more - * accurate estimates of the current track bitrates are available. */ - private int determineIdealSelectedIndex(long nowMs, int[] trackBitrates) { + private int determineIdealSelectedIndex(long nowMs) { long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth(); int lowestBitrateNonBlacklistedIndex = 0; for (int i = 0; i < length; i++) { if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { Format format = getFormat(i); - if (canSelectFormat(format, trackBitrates[i], playbackSpeed, effectiveBitrate)) { + if (canSelectFormat(format, format.bitrate, playbackSpeed, effectiveBitrate)) { return i; } else { lowestBitrateNonBlacklistedIndex = i; @@ -665,20 +606,26 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final BandwidthMeter bandwidthMeter; private final float bandwidthFraction; - - private long nonAllocatableBandwidth; + private final long reservedBandwidth; @Nullable private long[][] allocationCheckpoints; - /* package */ DefaultBandwidthProvider(BandwidthMeter bandwidthMeter, float bandwidthFraction) { + /* package */ + // the constructor does not initialize fields: allocationCheckpoints + @SuppressWarnings("nullness:initialization.fields.uninitialized") + DefaultBandwidthProvider( + BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) { this.bandwidthMeter = bandwidthMeter; this.bandwidthFraction = bandwidthFraction; + this.reservedBandwidth = reservedBandwidth; } + // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0] + @SuppressWarnings("nullness:unboxing.of.nullable") @Override public long getAllocatedBandwidth() { long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction); - long allocatableBandwidth = Math.max(0L, totalBandwidth - nonAllocatableBandwidth); + long allocatableBandwidth = Math.max(0L, totalBandwidth - reservedBandwidth); if (allocationCheckpoints == null) { return allocatableBandwidth; } @@ -694,10 +641,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1])); } - /* package */ void experimental_setNonAllocatableBandwidth(long nonAllocatableBandwidth) { - this.nonAllocatableBandwidth = nonAllocatableBandwidth; - } - /* package */ void experimental_setBandwidthAllocationCheckpoints( long[][] allocationCheckpoints) { Assertions.checkArgument(allocationCheckpoints.length >= 2); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java index 9826c5b13..b850a08ae 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.trackselection; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.Format; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index b38710a67..5330894da 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -19,18 +19,20 @@ import android.content.Context; import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -44,6 +46,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.nullness.compatqual.NullableType; /** @@ -184,12 +187,35 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean exceedRendererCapabilitiesIfNecessary; private int tunnelingAudioSessionId; - private final SparseArray> selectionOverrides; + private final SparseArray> + selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; - /** Creates a builder with default initial values. */ + /** + * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link + * #ParametersBuilder(Context)} instead. + */ + @Deprecated + @SuppressWarnings({"deprecation"}) public ParametersBuilder() { - this(Parameters.DEFAULT); + super(); + setInitialValuesWithoutContext(); + selectionOverrides = new SparseArray<>(); + rendererDisabledFlags = new SparseBooleanArray(); + } + + /** + * Creates a builder with default initial values. + * + * @param context Any context. + */ + + public ParametersBuilder(Context context) { + super(context); + setInitialValuesWithoutContext(); + selectionOverrides = new SparseArray<>(); + rendererDisabledFlags = new SparseBooleanArray(); + setViewportSizeToPhysicalDisplaySize(context, /* viewportOrientationMayChange= */ true); } /** @@ -329,7 +355,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size - * obtained from {@link Util#getPhysicalDisplaySize(Context)}. + * obtained from {@link Util#getCurrentDisplayModeSize(Context)}. * * @param context Any context. * @param viewportOrientationMayChange Whether the viewport orientation may change during @@ -339,7 +365,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public ParametersBuilder setViewportSizeToPhysicalDisplaySize( Context context, boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. - Point viewportSize = Util.getPhysicalDisplaySize(context); + Point viewportSize = Util.getCurrentDisplayModeSize(context); return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); } @@ -462,6 +488,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text + @Override + public ParametersBuilder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings( + Context context) { + super.setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context); + return this; + } + @Override public ParametersBuilder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { super.setPreferredTextLanguage(preferredTextLanguage); @@ -487,6 +520,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { super.setDisabledTextTrackSelectionFlags(disabledTextTrackSelectionFlags); return this; } + // General /** @@ -616,8 +650,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return This builder. */ public final ParametersBuilder setSelectionOverride( - int rendererIndex, TrackGroupArray groups, SelectionOverride override) { - Map overrides = selectionOverrides.get(rendererIndex); + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null) { overrides = new HashMap<>(); selectionOverrides.put(rendererIndex, overrides); @@ -639,7 +674,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final ParametersBuilder clearSelectionOverride( int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null || !overrides.containsKey(groups)) { // Nothing to clear. return this; @@ -658,7 +694,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return This builder. */ public final ParametersBuilder clearSelectionOverrides(int rendererIndex) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); if (overrides == null || overrides.isEmpty()) { // Nothing to clear. return this; @@ -719,9 +756,37 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererDisabledFlags); } - private static SparseArray> cloneSelectionOverrides( - SparseArray> selectionOverrides) { - SparseArray> clone = new SparseArray<>(); + private void setInitialValuesWithoutContext(@UnderInitialization ParametersBuilder this) { + // Video + maxVideoWidth = Integer.MAX_VALUE; + maxVideoHeight = Integer.MAX_VALUE; + maxVideoFrameRate = Integer.MAX_VALUE; + maxVideoBitrate = Integer.MAX_VALUE; + exceedVideoConstraintsIfNecessary = true; + allowVideoMixedMimeTypeAdaptiveness = false; + allowVideoNonSeamlessAdaptiveness = true; + viewportWidth = Integer.MAX_VALUE; + viewportHeight = Integer.MAX_VALUE; + viewportOrientationMayChange = true; + // Audio + maxAudioChannelCount = Integer.MAX_VALUE; + maxAudioBitrate = Integer.MAX_VALUE; + exceedAudioConstraintsIfNecessary = true; + allowAudioMixedMimeTypeAdaptiveness = false; + allowAudioMixedSampleRateAdaptiveness = false; + allowAudioMixedChannelCountAdaptiveness = false; + // General + forceLowestBitrate = false; + forceHighestSupportedBitrate = false; + exceedRendererCapabilitiesIfNecessary = true; + tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; + } + + private static SparseArray> + cloneSelectionOverrides( + SparseArray> selectionOverrides) { + SparseArray> clone = + new SparseArray<>(); for (int i = 0; i < selectionOverrides.size(); i++) { clone.put(selectionOverrides.keyAt(i), new HashMap<>(selectionOverrides.valueAt(i))); } @@ -735,8 +800,42 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public static final class Parameters extends TrackSelectionParameters { - /** An instance with default values. */ - public static final Parameters DEFAULT = new Parameters(); + /** + * An instance with default values, except those obtained from the {@link Context}. + * + *

If possible, use {@link #getDefaults(Context)} instead. + * + *

This instance will not have the following settings: + * + *

    + *
  • {@link ParametersBuilder#setViewportSizeToPhysicalDisplaySize(Context, boolean) + * Viewport constraints} configured for the primary display. + *
  • {@link + * ParametersBuilder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context) + * Preferred text language and role flags} configured to the accessibility settings of + * {@link android.view.accessibility.CaptioningManager}. + *
+ */ + @SuppressWarnings("deprecation") + public static final Parameters DEFAULT_WITHOUT_CONTEXT = new ParametersBuilder().build(); + + /** + * @deprecated This instance does not have {@link Context} constraints configured. Use {@link + * #getDefaults(Context)} instead. + */ + @Deprecated public static final Parameters DEFAULT_WITHOUT_VIEWPORT = DEFAULT_WITHOUT_CONTEXT; + + /** + * @deprecated This instance does not have {@link Context} constraints configured. Use {@link + * #getDefaults(Context)} instead. + */ + @Deprecated + public static final Parameters DEFAULT = DEFAULT_WITHOUT_CONTEXT; + + /** Returns an instance configured with default values. */ + public static Parameters getDefaults(Context context) { + return new ParametersBuilder(context).build(); + } // Video /** @@ -787,14 +886,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean allowVideoNonSeamlessAdaptiveness; /** * Viewport width in pixels. Constrains video track selections for adaptive content so that only - * tracks suitable for the viewport are selected. The default value is {@link Integer#MAX_VALUE} - * (i.e. no constraint). + * tracks suitable for the viewport are selected. The default value is the physical width of the + * primary display, in pixels. */ public final int viewportWidth; /** * Viewport height in pixels. Constrains video track selections for adaptive content so that - * only tracks suitable for the viewport are selected. The default value is {@link - * Integer#MAX_VALUE} (i.e. no constraint). + * only tracks suitable for the viewport are selected. The default value is the physical height + * of the primary display, in pixels. */ public final int viewportHeight; /** @@ -872,44 +971,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final int tunnelingAudioSessionId; // Overrides - private final SparseArray> selectionOverrides; + private final SparseArray> + selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; - private Parameters() { - this( - // Video - /* maxVideoWidth= */ Integer.MAX_VALUE, - /* maxVideoHeight= */ Integer.MAX_VALUE, - /* maxVideoFrameRate= */ Integer.MAX_VALUE, - /* maxVideoBitrate= */ Integer.MAX_VALUE, - /* exceedVideoConstraintsIfNecessary= */ true, - /* allowVideoMixedMimeTypeAdaptiveness= */ false, - /* allowVideoNonSeamlessAdaptiveness= */ true, - /* viewportWidth= */ Integer.MAX_VALUE, - /* viewportHeight= */ Integer.MAX_VALUE, - /* viewportOrientationMayChange= */ true, - // Audio - TrackSelectionParameters.DEFAULT.preferredAudioLanguage, - /* maxAudioChannelCount= */ Integer.MAX_VALUE, - /* maxAudioBitrate= */ Integer.MAX_VALUE, - /* exceedAudioConstraintsIfNecessary= */ true, - /* allowAudioMixedMimeTypeAdaptiveness= */ false, - /* allowAudioMixedSampleRateAdaptiveness= */ false, - /* allowAudioMixedChannelCountAdaptiveness= */ false, - // Text - TrackSelectionParameters.DEFAULT.preferredTextLanguage, - TrackSelectionParameters.DEFAULT.preferredTextRoleFlags, - TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, - TrackSelectionParameters.DEFAULT.disabledTextTrackSelectionFlags, - // General - /* forceLowestBitrate= */ false, - /* forceHighestSupportedBitrate= */ false, - /* exceedRendererCapabilitiesIfNecessary= */ true, - /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET, - new SparseArray<>(), - new SparseBooleanArray()); - } - /* package */ Parameters( // Video int maxVideoWidth, @@ -941,7 +1006,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean exceedRendererCapabilitiesIfNecessary, int tunnelingAudioSessionId, // Overrides - SparseArray> selectionOverrides, + SparseArray> selectionOverrides, SparseBooleanArray rendererDisabledFlags) { super( preferredAudioLanguage, @@ -1032,7 +1097,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return Whether there is an override. */ public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); return overrides != null && overrides.containsKey(groups); } @@ -1045,7 +1111,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Nullable public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { - Map overrides = selectionOverrides.get(rendererIndex); + Map overrides = + selectionOverrides.get(rendererIndex); return overrides != null ? overrides.get(groups) : null; } @@ -1178,17 +1245,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Static utility methods. - private static SparseArray> readSelectionOverrides( - Parcel in) { + private static SparseArray> + readSelectionOverrides(Parcel in) { int renderersWithOverridesCount = in.readInt(); - SparseArray> selectionOverrides = + SparseArray> selectionOverrides = new SparseArray<>(renderersWithOverridesCount); for (int i = 0; i < renderersWithOverridesCount; i++) { int rendererIndex = in.readInt(); int overrideCount = in.readInt(); - Map overrides = new HashMap<>(overrideCount); + Map overrides = + new HashMap<>(overrideCount); for (int j = 0; j < overrideCount; j++) { - TrackGroupArray trackGroups = in.readParcelable(TrackGroupArray.class.getClassLoader()); + TrackGroupArray trackGroups = + Assertions.checkNotNull(in.readParcelable(TrackGroupArray.class.getClassLoader())); + @Nullable SelectionOverride override = in.readParcelable(SelectionOverride.class.getClassLoader()); overrides.put(trackGroups, override); } @@ -1198,16 +1268,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static void writeSelectionOverridesToParcel( - Parcel dest, SparseArray> selectionOverrides) { + Parcel dest, + SparseArray> selectionOverrides) { int renderersWithOverridesCount = selectionOverrides.size(); dest.writeInt(renderersWithOverridesCount); for (int i = 0; i < renderersWithOverridesCount; i++) { int rendererIndex = selectionOverrides.keyAt(i); - Map overrides = selectionOverrides.valueAt(i); + Map overrides = + selectionOverrides.valueAt(i); int overrideCount = overrides.size(); dest.writeInt(rendererIndex); dest.writeInt(overrideCount); - for (Map.Entry override : overrides.entrySet()) { + for (Map.Entry override : + overrides.entrySet()) { dest.writeParcelable(override.getKey(), /* parcelableFlags= */ 0); dest.writeParcelable(override.getValue(), /* parcelableFlags= */ 0); } @@ -1230,8 +1303,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static boolean areSelectionOverridesEqual( - SparseArray> first, - SparseArray> second) { + SparseArray> first, + SparseArray> second) { int firstSize = first.size(); if (second.size() != firstSize) { return false; @@ -1248,13 +1321,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static boolean areSelectionOverridesEqual( - Map first, - Map second) { + Map first, + Map second) { int firstSize = first.size(); if (second.size() != firstSize) { return false; } - for (Map.Entry firstEntry : first.entrySet()) { + for (Map.Entry firstEntry : + first.entrySet()) { TrackGroupArray key = firstEntry.getKey(); if (!second.containsKey(key) || !Util.areEqual(firstEntry.getValue(), second.get(key))) { return false; @@ -1382,13 +1456,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean allowMultipleAdaptiveSelections; + /** @deprecated Use {@link #DefaultTrackSelector(Context)} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public DefaultTrackSelector() { this(new AdaptiveTrackSelection.Factory()); } /** - * @deprecated Use {@link #DefaultTrackSelector()} instead. Custom bandwidth meter should be - * directly passed to the player in {@link ExoPlayerFactory}. + * @deprecated Use {@link #DefaultTrackSelector(Context)} instead. The bandwidth meter should be + * passed directly to the player in {@link + * com.google.android.exoplayer2.SimpleExoPlayer.Builder}. */ @Deprecated @SuppressWarnings("deprecation") @@ -1396,10 +1474,32 @@ public class DefaultTrackSelector extends MappingTrackSelector { this(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } - /** @param trackSelectionFactory A factory for {@link TrackSelection}s. */ + /** @deprecated Use {@link #DefaultTrackSelector(Context, TrackSelection.Factory)}. */ + @Deprecated public DefaultTrackSelector(TrackSelection.Factory trackSelectionFactory) { + this(Parameters.DEFAULT_WITHOUT_CONTEXT, trackSelectionFactory); + } + + /** @param context Any {@link Context}. */ + public DefaultTrackSelector(Context context) { + this(context, new AdaptiveTrackSelection.Factory()); + } + + /** + * @param context Any {@link Context}. + * @param trackSelectionFactory A factory for {@link TrackSelection}s. + */ + public DefaultTrackSelector(Context context, TrackSelection.Factory trackSelectionFactory) { + this(Parameters.getDefaults(context), trackSelectionFactory); + } + + /** + * @param parameters Initial {@link Parameters}. + * @param trackSelectionFactory A factory for {@link TrackSelection}s. + */ + public DefaultTrackSelector(Parameters parameters, TrackSelection.Factory trackSelectionFactory) { this.trackSelectionFactory = trackSelectionFactory; - parametersReference = new AtomicReference<>(Parameters.DEFAULT); + parametersReference = new AtomicReference<>(parameters); } /** @@ -1443,7 +1543,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { setParameters(buildUponParameters().setRendererDisabled(rendererIndex, disabled)); } - /** @deprecated Use {@link Parameters#getRendererDisabled(int)}. * */ + /** @deprecated Use {@link Parameters#getRendererDisabled(int)}. */ @Deprecated public final boolean getRendererDisabled(int rendererIndex) { return getParameters().getRendererDisabled(rendererIndex); @@ -1455,11 +1555,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Deprecated public final void setSelectionOverride( - int rendererIndex, TrackGroupArray groups, SelectionOverride override) { + int rendererIndex, TrackGroupArray groups, @Nullable SelectionOverride override) { setParameters(buildUponParameters().setSelectionOverride(rendererIndex, groups, override)); } - /** @deprecated Use {@link Parameters#hasSelectionOverride(int, TrackGroupArray)}. * */ + /** @deprecated Use {@link Parameters#hasSelectionOverride(int, TrackGroupArray)}. */ @Deprecated public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) { return getParameters().hasSelectionOverride(rendererIndex, groups); @@ -1511,8 +1611,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports) throws ExoPlaybackException { Parameters params = parametersReference.get(); int rendererCount = mappedTrackInfo.getRendererCount(); @@ -1581,18 +1681,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { * generated by this method will be overridden to account for these properties. * * @param mappedTrackInfo Mapped track information. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). - * @param rendererMixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @return The {@link TrackSelection.Definition}s for the renderers. A null entry indicates no * selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection.@NullableType Definition[] selectAllTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports, + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); @@ -1696,10 +1796,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for a video renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). - * @param mixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @param params The selector's current constraint parameters. * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. * @return The {@link TrackSelection.Definition} for the renderer, or null if no selection was @@ -1709,8 +1809,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected TrackSelection.Definition selectVideoTrack( TrackGroupArray groups, - int[][] formatSupports, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException { @@ -1730,8 +1830,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectAdaptiveVideoTrack( TrackGroupArray groups, - int[][] formatSupport, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupport, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params) { int requiredAdaptiveSupport = params.allowVideoNonSeamlessAdaptiveness @@ -1764,7 +1864,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveVideoTracksForGroup( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, @@ -1829,7 +1929,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int getAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1857,7 +1957,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void filterAdaptiveVideoTrackCountForMimeType( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int requiredAdaptiveSupport, @Nullable String mimeType, int maxVideoWidth, @@ -1884,12 +1984,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static boolean isSupportedAdaptiveVideoTrack( Format format, @Nullable String mimeType, - int formatSupport, + @Capabilities int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoFrameRate, int maxVideoBitrate) { + if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) { + // Ignore trick-play tracks for now. + return false; + } return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) @@ -1901,7 +2005,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectFixedVideoTrack( - TrackGroupArray groups, int[][] formatSupports, Parameters params) { + TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -1911,11 +2015,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); - int[] trackFormatSupport = formatSupports[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { + Format format = trackGroup.getFormat(trackIndex); + if ((format.roleFlags & C.ROLE_FLAG_TRICK_PLAY) != 0) { + // Ignore trick-play tracks for now. + continue; + } if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { - Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth) @@ -1974,10 +2082,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for an audio renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). - * @param mixedMimeTypeAdaptationSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for the renderer. + * @param formatSupports The {@link Capabilities} for each mapped track, indexed by renderer, + * track group and track (in that order). + * @param mixedMimeTypeAdaptationSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @param params The selector's current constraint parameters. * @param enableAdaptiveTrackSelection Whether adaptive track selection is allowed. * @return The {@link TrackSelection.Definition} and corresponding {@link AudioTrackScore}, or @@ -1988,8 +2096,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected Pair selectAudioTrack( TrackGroupArray groups, - int[][] formatSupports, - int mixedMimeTypeAdaptationSupports, + @Capabilities int[][] formatSupports, + @AdaptiveSupport int mixedMimeTypeAdaptationSupports, Parameters params, boolean enableAdaptiveTrackSelection) throws ExoPlaybackException { @@ -1998,7 +2106,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { AudioTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupports[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2051,7 +2159,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveAudioTracks( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, boolean allowMixedSampleRateAdaptiveness, @@ -2105,7 +2213,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int getAdaptiveAudioTrackCount( TrackGroup group, - int[] formatSupport, + @Capabilities int[] formatSupport, AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, @@ -2129,7 +2237,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static boolean isSupportedAdaptiveAudioTrack( Format format, - int formatSupport, + @Capabilities int formatSupport, AudioConfigurationTuple configuration, int maxAudioBitrate, boolean allowMixedMimeTypeAdaptiveness, @@ -2155,8 +2263,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link TrackSelection} for a text renderer. * * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). * @param params The selector's current constraint parameters. * @param selectedAudioLanguage The language of the selected audio track. May be null if the * selected text track declares no language or no text track was selected. @@ -2167,7 +2275,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable protected Pair selectTextTrack( TrackGroupArray groups, - int[][] formatSupport, + @Capabilities int[][] formatSupport, Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { @@ -2176,7 +2284,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2208,22 +2316,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param trackType The type of the renderer. * @param groups The {@link TrackGroupArray} mapped to the renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped - * track, indexed by track group index and track index (in that order). + * @param formatSupport The {@link Capabilities} for each mapped track, indexed by renderer, track + * group and track (in that order). * @param params The selector's current constraint parameters. * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ @Nullable protected TrackSelection.Definition selectOtherTrack( - int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params) + int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - int[] trackFormatSupport = formatSupport[groupIndex]; + @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { @@ -2254,6 +2362,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderers if so. * * @param mappedTrackInfo Mapped track information. + * @param renderererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). * @param rendererConfigurations The renderer configurations. Configurations may be replaced with * ones that enable tunneling as a result of this call. * @param trackSelections The renderer track selections. @@ -2262,7 +2372,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, - int[][][] renderererFormatSupports, + @Capabilities int[][][] renderererFormatSupports, @NullableType RendererConfiguration[] rendererConfigurations, @NullableType TrackSelection[] trackSelections, int tunnelingAudioSessionId) { @@ -2311,21 +2421,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns whether a renderer supports tunneling for a {@link TrackSelection}. * - * @param formatSupports The result of {@link RendererCapabilities#supportsFormat} for each track, - * indexed by group index and track index (in that order). + * @param formatSupports The {@link Capabilities} for each track, indexed by group index and track + * index (in that order). * @param trackGroups The {@link TrackGroupArray}s for the renderer. * @param selection The track selection. * @return Whether the renderer supports tunneling for the {@link TrackSelection}. */ private static boolean rendererSupportsTunneling( - int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { + @Capabilities int[][] formatSupports, TrackGroupArray trackGroups, TrackSelection selection) { if (selection == null) { return false; } int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); for (int i = 0; i < selection.length(); i++) { + @Capabilities int trackFormatSupport = formatSupports[trackGroupIndex][selection.getIndexInTrackGroup(i)]; - if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK) + if (RendererCapabilities.getTunnelingSupport(trackFormatSupport) != RendererCapabilities.TUNNELING_SUPPORTED) { return false; } @@ -2349,20 +2460,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from - * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is - * {@link RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set - * and the result is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link + * RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the + * format support is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. * - * @param formatSupport A value obtained from {@link RendererCapabilities#supportsFormat(Format)}. - * @param allowExceedsCapabilities Whether to return true if the format support component of the - * value is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. - * @return True if the format support component is {@link RendererCapabilities#FORMAT_HANDLED}, or - * if {@code allowExceedsCapabilities} is set and the format support component is - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @param formatSupport {@link Capabilities}. + * @param allowExceedsCapabilities Whether to return true if {@link FormatSupport} is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @return True if {@link FormatSupport} is {@link RendererCapabilities#FORMAT_HANDLED}, or if + * {@code allowExceedsCapabilities} is set and the format support is {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. */ - protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { - int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; + protected static boolean isSupported( + @Capabilities int formatSupport, boolean allowExceedsCapabilities) { + @FormatSupport int maskedSupport = RendererCapabilities.getFormatSupport(formatSupport); return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } @@ -2518,7 +2629,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final int sampleRate; private final int bitrate; - public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { + public AudioTrackScore(Format format, Parameters parameters, @Capabilities int formatSupport) { this.parameters = parameters; this.language = normalizeUndeterminedLanguageToNull(format.language); isWithinRendererCapabilities = isSupported(formatSupport, false); @@ -2657,7 +2768,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public TextTrackScore( Format format, Parameters parameters, - int trackFormatSupport, + @Capabilities int trackFormatSupport, @Nullable String selectedAudioLanguage) { isWithinRendererCapabilities = isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index 3bdaeeeaf..fefad00cb 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -39,7 +39,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { public static final class Factory implements TrackSelection.Factory { private final int reason; - private final @Nullable Object data; + @Nullable private final Object data; public Factory() { this.reason = C.SELECTION_REASON_UNKNOWN; @@ -66,7 +66,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { } private final int reason; - private final @Nullable Object data; + @Nullable private final Object data; /** * @param group The {@link TrackGroup}. Must not be null. @@ -109,7 +109,8 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public Object getSelectionData() { return data; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 2738ee592..f6ba1f259 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -15,18 +15,22 @@ */ package com.google.android.exoplayer2.trackselection; +import android.util.Pair; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; +import com.google.android.exoplayer2.RendererCapabilities.Capabilities; +import com.google.android.exoplayer2.RendererCapabilities.FormatSupport; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -90,25 +94,25 @@ public abstract class MappingTrackSelector extends TrackSelector { private final int rendererCount; private final int[] rendererTrackTypes; private final TrackGroupArray[] rendererTrackGroups; - private final int[] rendererMixedMimeTypeAdaptiveSupports; - private final int[][][] rendererFormatSupports; + @AdaptiveSupport private final int[] rendererMixedMimeTypeAdaptiveSupports; + @Capabilities private final int[][][] rendererFormatSupports; private final TrackGroupArray unmappedTrackGroups; /** * @param rendererTrackTypes The track type handled by each renderer. * @param rendererTrackGroups The {@link TrackGroup}s mapped to each renderer. - * @param rendererMixedMimeTypeAdaptiveSupports The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptiveSupports The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. + * @param rendererFormatSupports The {@link Capabilities} for each mapped track, indexed by + * renderer, track group and track (in that order). * @param unmappedTrackGroups {@link TrackGroup}s not mapped to any renderer. */ @SuppressWarnings("deprecation") /* package */ MappedTrackInfo( int[] rendererTrackTypes, TrackGroupArray[] rendererTrackGroups, - int[] rendererMixedMimeTypeAdaptiveSupports, - int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptiveSupports, + @Capabilities int[][][] rendererFormatSupports, TrackGroupArray unmappedTrackGroups) { this.rendererTrackTypes = rendererTrackTypes; this.rendererTrackGroups = rendererTrackGroups; @@ -149,25 +153,28 @@ public abstract class MappingTrackSelector extends TrackSelector { * Returns the extent to which a renderer can play the tracks that are mapped to it. * * @param rendererIndex The renderer index. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link - * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link - * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return The {@link RendererSupport}. */ - public @RendererSupport int getRendererSupport(int rendererIndex) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; - for (int i = 0; i < rendererFormatSupport.length; i++) { - for (int j = 0; j < rendererFormatSupport[i].length; j++) { + @RendererSupport + public int getRendererSupport(int rendererIndex) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @Capabilities int[][] rendererFormatSupport = rendererFormatSupports[rendererIndex]; + for (@Capabilities int[] trackGroupFormatSupport : rendererFormatSupport) { + for (@Capabilities int trackFormatSupport : trackGroupFormatSupport) { int trackRendererSupport; - switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) { + switch (RendererCapabilities.getFormatSupport(trackFormatSupport)) { case RendererCapabilities.FORMAT_HANDLED: return RENDERER_SUPPORT_PLAYABLE_TRACKS; case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; break; - default: + case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; break; + default: + throw new IllegalStateException(); } bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); } @@ -177,7 +184,8 @@ public abstract class MappingTrackSelector extends TrackSelector { /** @deprecated Use {@link #getTypeSupport(int)}. */ @Deprecated - public @RendererSupport int getTrackTypeRendererSupport(int trackType) { + @RendererSupport + public int getTrackTypeRendererSupport(int trackType) { return getTypeSupport(trackType); } @@ -188,12 +196,11 @@ public abstract class MappingTrackSelector extends TrackSelector { * returned. * * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, {@link - * #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, {@link - * #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + * @return The {@link RendererSupport}. */ - public @RendererSupport int getTypeSupport(int trackType) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + @RendererSupport + public int getTypeSupport(int trackType) { + @RendererSupport int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; for (int i = 0; i < rendererCount; i++) { if (rendererTrackTypes[i] == trackType) { bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); @@ -204,6 +211,7 @@ public abstract class MappingTrackSelector extends TrackSelector { /** @deprecated Use {@link #getTrackSupport(int, int, int)}. */ @Deprecated + @FormatSupport public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { return getTrackSupport(rendererIndex, groupIndex, trackIndex); } @@ -214,15 +222,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param rendererIndex The renderer index. * @param groupIndex The index of the track group to which the track belongs. * @param trackIndex The index of the track within the track group. - * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, {@link - * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and {@link - * RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. + * @return The {@link FormatSupport}. */ + @FormatSupport public int getTrackSupport(int rendererIndex, int groupIndex, int trackIndex) { - return rendererFormatSupports[rendererIndex][groupIndex][trackIndex] - & RendererCapabilities.FORMAT_SUPPORT_MASK; + return RendererCapabilities.getFormatSupport( + rendererFormatSupports[rendererIndex][groupIndex][trackIndex]); } /** @@ -242,10 +247,9 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param groupIndex The index of the track group. * @param includeCapabilitiesExceededTracks Whether tracks that exceed the capabilities of the * renderer are included when determining support. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link - * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link - * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport}. */ + @AdaptiveSupport public int getAdaptiveSupport( int rendererIndex, int groupIndex, boolean includeCapabilitiesExceededTracks) { int trackCount = rendererTrackGroups[rendererIndex].get(groupIndex).length; @@ -253,7 +257,7 @@ public abstract class MappingTrackSelector extends TrackSelector { int[] trackIndices = new int[trackCount]; int trackIndexCount = 0; for (int i = 0; i < trackCount; i++) { - int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); + @FormatSupport int fixedSupport = getTrackSupport(rendererIndex, groupIndex, i); if (fixedSupport == RendererCapabilities.FORMAT_HANDLED || (includeCapabilitiesExceededTracks && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { @@ -270,13 +274,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * * @param rendererIndex The renderer index. * @param groupIndex The index of the track group. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, {@link - * RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and {@link - * RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + * @return The {@link AdaptiveSupport}. */ + @AdaptiveSupport public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { int handledTrackCount = 0; - int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; + @AdaptiveSupport int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; boolean multipleMimeTypes = false; String firstSampleMimeType = null; for (int i = 0; i < trackIndices.length; i++) { @@ -291,8 +294,8 @@ public abstract class MappingTrackSelector extends TrackSelector { adaptiveSupport = Math.min( adaptiveSupport, - rendererFormatSupports[rendererIndex][groupIndex][i] - & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); + RendererCapabilities.getAdaptiveSupport( + rendererFormatSupports[rendererIndex][groupIndex][i])); } return multipleMimeTypes ? Math.min(adaptiveSupport, rendererMixedMimeTypeAdaptiveSupports[rendererIndex]) @@ -312,7 +315,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } - private @Nullable MappedTrackInfo currentMappedTrackInfo; + @Nullable private MappedTrackInfo currentMappedTrackInfo; /** * Returns the mapping information for the currently active track selection, or null if no @@ -341,13 +344,14 @@ public abstract class MappingTrackSelector extends TrackSelector { // any renderer. int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1]; TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][]; - int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; + @Capabilities int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][]; for (int i = 0; i < rendererTrackGroups.length; i++) { rendererTrackGroups[i] = new TrackGroup[trackGroups.length]; rendererFormatSupports[i] = new int[trackGroups.length][]; } // Determine the extent to which each renderer supports mixed mimeType adaptation. + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupports = getMixedMimeTypeAdaptationSupports(rendererCapabilities); @@ -356,10 +360,17 @@ public abstract class MappingTrackSelector extends TrackSelector { for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { TrackGroup group = trackGroups.get(groupIndex); // Associate the group to a preferred renderer. - int rendererIndex = findRenderer(rendererCapabilities, group); + boolean preferUnassociatedRenderer = + MimeTypes.getTrackType(group.getFormat(0).sampleMimeType) == C.TRACK_TYPE_METADATA; + int rendererIndex = + findRenderer( + rendererCapabilities, group, rendererTrackGroupCounts, preferUnassociatedRenderer); // Evaluate the support that the renderer provides for each track in the group. - int[] rendererFormatSupport = rendererIndex == rendererCapabilities.length - ? new int[group.length] : getFormatSupport(rendererCapabilities[rendererIndex], group); + @Capabilities + int[] rendererFormatSupport = + rendererIndex == rendererCapabilities.length + ? new int[group.length] + : getFormatSupport(rendererCapabilities[rendererIndex], group); // Stash the results. int rendererTrackGroupCount = rendererTrackGroupCounts[rendererIndex]; rendererTrackGroups[rendererIndex][rendererTrackGroupCount] = group; @@ -406,10 +417,10 @@ public abstract class MappingTrackSelector extends TrackSelector { * Given mapped track information, returns a track selection and configuration for each renderer. * * @param mappedTrackInfo Mapped track information. - * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for - * each mapped track, indexed by renderer, track group and track (in that order). - * @param rendererMixedMimeTypeAdaptationSupport The result of {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param rendererFormatSupports The {@link Capabilities} for ach mapped track, indexed by + * renderer, track group and track (in that order). + * @param rendererMixedMimeTypeAdaptationSupport The {@link AdaptiveSupport} for mixed MIME type + * adaptation for the renderer. * @return A pair consisting of the track selections and configurations for each renderer. A null * configuration indicates the renderer should be disabled, in which case the track selection * will also be null. A track selection may also be null for a non-disabled renderer if {@link @@ -419,65 +430,89 @@ public abstract class MappingTrackSelector extends TrackSelector { protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> selectTracks( MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupport) + @Capabilities int[][][] rendererFormatSupports, + @AdaptiveSupport int[] rendererMixedMimeTypeAdaptationSupport) throws ExoPlaybackException; /** * Finds the renderer to which the provided {@link TrackGroup} should be mapped. - *

- * A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in - * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. In the case that two or more renderers - * report the same level of support, the renderer with the lowest index is associated. - *

- * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the + * + *

A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in + * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, {@link + * RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and {@link + * RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. + * + *

In the case that two or more renderers report the same level of support, the assignment + * depends on {@code preferUnassociatedRenderer}. + * + *

    + *
  • If {@code preferUnassociatedRenderer} is false, the renderer with the lowest index is + * chosen regardless of how many other track groups are already mapped to this renderer. + *
  • If {@code preferUnassociatedRenderer} is true, the renderer with the lowest index and no + * other mapped track group is chosen, or the renderer with the lowest index if all + * available renderers have already mapped track groups. + *
+ * + *

If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the * tracks in the group, then {@code renderers.length} is returned to indicate that the group was * not mapped to any renderer. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. * @param group The track group to map to a renderer. - * @return The index of the renderer to which the track group was mapped, or - * {@code renderers.length} if it was not mapped to any renderer. + * @param rendererTrackGroupCounts The number of already mapped track groups for each renderer. + * @param preferUnassociatedRenderer Whether renderers unassociated to any track group should be + * preferred. + * @return The index of the renderer to which the track group was mapped, or {@code + * renderers.length} if it was not mapped to any renderer. * @throws ExoPlaybackException If an error occurs finding a renderer. */ - private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group) + private static int findRenderer( + RendererCapabilities[] rendererCapabilities, + TrackGroup group, + int[] rendererTrackGroupCounts, + boolean preferUnassociatedRenderer) throws ExoPlaybackException { int bestRendererIndex = rendererCapabilities.length; - int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + @FormatSupport int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; + boolean bestRendererIsUnassociated = true; for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) { RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex]; + @FormatSupport int formatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { - int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex)) - & RendererCapabilities.FORMAT_SUPPORT_MASK; - if (formatSupportLevel > bestFormatSupportLevel) { - bestRendererIndex = rendererIndex; - bestFormatSupportLevel = formatSupportLevel; - if (bestFormatSupportLevel == RendererCapabilities.FORMAT_HANDLED) { - // We can't do better. - return bestRendererIndex; - } - } + @FormatSupport + int trackFormatSupportLevel = + RendererCapabilities.getFormatSupport( + rendererCapability.supportsFormat(group.getFormat(trackIndex))); + formatSupportLevel = Math.max(formatSupportLevel, trackFormatSupportLevel); + } + boolean rendererIsUnassociated = rendererTrackGroupCounts[rendererIndex] == 0; + if (formatSupportLevel > bestFormatSupportLevel + || (formatSupportLevel == bestFormatSupportLevel + && preferUnassociatedRenderer + && !bestRendererIsUnassociated + && rendererIsUnassociated)) { + bestRendererIndex = rendererIndex; + bestFormatSupportLevel = formatSupportLevel; + bestRendererIsUnassociated = rendererIsUnassociated; } } return bestRendererIndex; } /** - * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified - * {@link TrackGroup}, returning the results in an array. + * Calls {@link RendererCapabilities#supportsFormat} for each track in the specified {@link + * TrackGroup}, returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderer. * @param group The track group to evaluate. - * @return An array containing the result of calling - * {@link RendererCapabilities#supportsFormat} for each track in the group. + * @return An array containing {@link Capabilities} for each track in the group. * @throws ExoPlaybackException If an error occurs determining the format support. */ + @Capabilities private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group) throws ExoPlaybackException { - int[] formatSupport = new int[group.length]; + @Capabilities int[] formatSupport = new int[group.length]; for (int i = 0; i < group.length; i++) { formatSupport[i] = rendererCapabilities.supportsFormat(group.getFormat(i)); } @@ -489,13 +524,14 @@ public abstract class MappingTrackSelector extends TrackSelector { * returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @return An array containing the result of calling {@link - * RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @return An array containing the {@link AdaptiveSupport} for mixed MIME type adaptation for the + * renderer. * @throws ExoPlaybackException If an error occurs determining the adaptation support. */ + @AdaptiveSupport private static int[] getMixedMimeTypeAdaptationSupports( RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException { - int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; + @AdaptiveSupport int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length]; for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) { mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index 805321296..f35e7ec75 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -135,7 +135,8 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public @Nullable Object getSelectionData() { + @Nullable + public Object getSelectionData() { return null; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java deleted file mode 100644 index 1cd6c09bf..000000000 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackBitrateEstimator.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.exoplayer2.trackselection; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import java.util.List; - -/** Estimates track bitrate values. */ -public interface TrackBitrateEstimator { - - /** - * A {@link TrackBitrateEstimator} that returns the bitrate values defined in the track formats. - */ - TrackBitrateEstimator DEFAULT = - (formats, queue, iterators, bitrates) -> - TrackSelectionUtil.getFormatBitrates(formats, bitrates); - - /** - * Returns bitrate values for a set of tracks whose formats are given. - * - * @param formats The track formats. - * @param queue The queue of already buffered {@link MediaChunk} instances. Must not be modified. - * @param iterators An array of {@link MediaChunkIterator}s providing information about the - * sequence of upcoming media chunks for each track. - * @param bitrates An array into which the bitrate values will be written. If non-null, this array - * is the one that will be returned. - * @return Bitrate values for the tracks. As long as the format of a track has set bitrate, a - * bitrate value is set in the returned array. Otherwise it might be set to {@link - * Format#NO_VALUE}. - */ - int[] getBitrates( - Format[] formats, - List queue, - MediaChunkIterator[] iterators, - @Nullable int[] bitrates); -} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index bd99403b0..ad1a6ef1f 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.trackselection.TrackSelectionUtil.AdaptiveTrackSelectionFactory; import com.google.android.exoplayer2.upstream.BandwidthMeter; import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; @@ -77,32 +76,19 @@ public interface TrackSelection { interface Factory { /** - * @deprecated Implement {@link #createTrackSelections(Definition[], BandwidthMeter)} instead. - * Calling {@link TrackSelectionUtil#createTrackSelectionsForDefinitions(Definition[], - * AdaptiveTrackSelectionFactory)} helps to create a single adaptive track selection in the - * same way as using this deprecated method. - */ - @Deprecated - default TrackSelection createTrackSelection( - TrackGroup group, BandwidthMeter bandwidthMeter, int... tracks) { - throw new UnsupportedOperationException(); - } - - /** - * Creates a new selection for each {@link Definition}. + * Creates track selections for the provided {@link Definition Definitions}. + * + *

Implementations that create at most one adaptive track selection may use {@link + * TrackSelectionUtil#createTrackSelectionsForDefinitions}. * * @param definitions A {@link Definition} array. May include null values. * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks. * @return The created selections. Must have the same length as {@code definitions} and may * include null values. */ - @SuppressWarnings("deprecation") - default @NullableType TrackSelection[] createTrackSelections( - @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) { - return TrackSelectionUtil.createTrackSelectionsForDefinitions( - definitions, - definition -> createTrackSelection(definition.group, bandwidthMeter, definition.tracks)); - } + @NullableType + TrackSelection[] createTrackSelections( + @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter); } /** @@ -213,16 +199,6 @@ public interface TrackSelection { */ default void onDiscontinuity() {} - /** - * @deprecated Use and implement {@link #updateSelectedTrack(long, long, long, List, - * MediaChunkIterator[])} instead. - */ - @Deprecated - default void updateSelectedTrack( - long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { - throw new UnsupportedOperationException(); - } - /** * Updates the selected track for sources that load media in discrete {@link MediaChunk}s. * @@ -247,14 +223,12 @@ public interface TrackSelection { * that this information may not be available for all tracks, and so some iterators may be * empty. */ - default void updateSelectedTrack( + void updateSelectedTrack( long playbackPositionUs, long bufferedDurationUs, long availableDurationUs, List queue, - MediaChunkIterator[] mediaChunkIterators) { - updateSelectedTrack(playbackPositionUs, bufferedDurationUs, availableDurationUs); - } + MediaChunkIterator[] mediaChunkIterators); /** * May be called periodically by sources that load media in discrete {@link MediaChunk}s and diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index bc905ace4..fc20e863b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -42,7 +42,8 @@ public final class TrackSelectionArray { * @param index The index of the selection. * @return The selection. */ - public @Nullable TrackSelection get(int index) { + @Nullable + public TrackSelection get(int index) { return trackSelections[index]; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 047791387..6e10171f0 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -15,12 +15,17 @@ */ package com.google.android.exoplayer2.trackselection; +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.Nullable; import android.text.TextUtils; +import android.view.accessibility.CaptioningManager; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; +import java.util.Locale; /** Constraint parameters for track selection. */ public class TrackSelectionParameters implements Parcelable { @@ -31,17 +36,34 @@ public class TrackSelectionParameters implements Parcelable { */ public static class Builder { - // Audio @Nullable /* package */ String preferredAudioLanguage; - // Text @Nullable /* package */ String preferredTextLanguage; @C.RoleFlags /* package */ int preferredTextRoleFlags; /* package */ boolean selectUndeterminedTextLanguage; @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags; - /** Creates a builder with default initial values. */ + /** + * Creates a builder with default initial values. + * + * @param context Any context. + */ + @SuppressWarnings({"deprecation", "initialization:method.invocation.invalid"}) + public Builder(Context context) { + this(); + setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(context); + } + + /** + * @deprecated {@link Context} constraints will not be set when using this constructor. Use + * {@link #Builder(Context)} instead. + */ + @Deprecated public Builder() { - this(DEFAULT); + preferredAudioLanguage = null; + preferredTextLanguage = null; + preferredTextRoleFlags = 0; + selectUndeterminedTextLanguage = false; + disabledTextTrackSelectionFlags = 0; } /** @@ -49,9 +71,7 @@ public class TrackSelectionParameters implements Parcelable { * the builder are obtained. */ /* package */ Builder(TrackSelectionParameters initialValues) { - // Audio preferredAudioLanguage = initialValues.preferredAudioLanguage; - // Text preferredTextLanguage = initialValues.preferredTextLanguage; preferredTextRoleFlags = initialValues.preferredTextRoleFlags; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; @@ -70,7 +90,22 @@ public class TrackSelectionParameters implements Parcelable { return this; } - // Text + /** + * Sets the preferred language and role flags for text tracks based on the accessibility + * settings of {@link CaptioningManager}. + * + *

Does nothing for API levels < 19 or when the {@link CaptioningManager} is disabled. + * + * @param context A {@link Context}. + * @return This builder. + */ + public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings( + Context context) { + if (Util.SDK_INT >= 19) { + setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19(context); + } + return this; + } /** * Sets the preferred language for text tracks. @@ -133,26 +168,72 @@ public class TrackSelectionParameters implements Parcelable { selectUndeterminedTextLanguage, disabledTextTrackSelectionFlags); } + + @TargetApi(19) + private void setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettingsV19( + Context context) { + if (Util.SDK_INT < 23 && Looper.myLooper() == null) { + // Android platform bug (pre-Marshmallow) that causes RuntimeExceptions when + // CaptioningService is instantiated from a non-Looper thread. See [internal: b/143779904]. + return; + } + CaptioningManager captioningManager = + (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); + if (captioningManager == null || !captioningManager.isEnabled()) { + return; + } + preferredTextRoleFlags = C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND; + Locale preferredLocale = captioningManager.getLocale(); + if (preferredLocale != null) { + preferredTextLanguage = Util.getLocaleLanguageTag(preferredLocale); + } + } } - /** An instance with default values. */ - public static final TrackSelectionParameters DEFAULT = new TrackSelectionParameters(); + /** + * An instance with default values, except those obtained from the {@link Context}. + * + *

If possible, use {@link #getDefaults(Context)} instead. + * + *

This instance will not have the following settings: + * + *

    + *
  • {@link Builder#setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(Context) + * Preferred text language and role flags} configured to the accessibility settings of + * {@link CaptioningManager}. + *
+ */ + @SuppressWarnings("deprecation") + public static final TrackSelectionParameters DEFAULT_WITHOUT_CONTEXT = new Builder().build(); /** - * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null} - * selects the default track, or the first track if there's no default. The default value is - * {@code null}. + * @deprecated This instance is not configured using {@link Context} constraints. Use {@link + * #getDefaults(Context)} instead. + */ + @Deprecated public static final TrackSelectionParameters DEFAULT = DEFAULT_WITHOUT_CONTEXT; + + /** Returns an instance configured with default values. */ + public static TrackSelectionParameters getDefaults(Context context) { + return new Builder(context).build(); + } + + /** + * The preferred language for audio and forced text tracks as an IETF BCP 47 conformant tag. + * {@code null} selects the default track, or the first track if there's no default. The default + * value is {@code null}. */ @Nullable public final String preferredAudioLanguage; - // Text /** - * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the default - * track if there is one, or no track otherwise. The default value is {@code null}. + * The preferred language for text tracks as an IETF BCP 47 conformant tag. {@code null} selects + * the default track if there is one, or no track otherwise. The default value is {@code null}, or + * the language of the accessibility {@link CaptioningManager} if enabled. */ @Nullable public final String preferredTextLanguage; /** * The preferred {@link C.RoleFlags} for text tracks. {@code 0} selects the default track if there - * is one, or no track otherwise. The default value is {@code 0}. + * is one, or no track otherwise. The default value is {@code 0}, or {@link C#ROLE_FLAG_SUBTITLE} + * | {@link C#ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND} if the accessibility {@link CaptioningManager} + * is enabled. */ @C.RoleFlags public final int preferredTextRoleFlags; /** @@ -167,16 +248,6 @@ public class TrackSelectionParameters implements Parcelable { */ @C.SelectionFlags public final int disabledTextTrackSelectionFlags; - /* package */ TrackSelectionParameters() { - this( - /* preferredAudioLanguage= */ null, - // Text - /* preferredTextLanguage= */ null, - /* preferredTextRoleFlags= */ 0, - /* selectUndeterminedTextLanguage= */ false, - /* disabledTextTrackSelectionFlags= */ 0); - } - /* package */ TrackSelectionParameters( @Nullable String preferredAudioLanguage, @Nullable String preferredTextLanguage, @@ -193,9 +264,7 @@ public class TrackSelectionParameters implements Parcelable { } /* package */ TrackSelectionParameters(Parcel in) { - // Audio this.preferredAudioLanguage = in.readString(); - // Text this.preferredTextLanguage = in.readString(); this.preferredTextRoleFlags = in.readInt(); this.selectUndeterminedTextLanguage = Util.readBoolean(in); @@ -218,7 +287,6 @@ public class TrackSelectionParameters implements Parcelable { } TrackSelectionParameters other = (TrackSelectionParameters) obj; return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) - // Text && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) && preferredTextRoleFlags == other.preferredTextRoleFlags && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage @@ -228,9 +296,7 @@ public class TrackSelectionParameters implements Parcelable { @Override public int hashCode() { int result = 1; - // Audio result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); - // Text result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); result = 31 * result + preferredTextRoleFlags; result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); @@ -247,9 +313,7 @@ public class TrackSelectionParameters implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - // Audio dest.writeString(preferredAudioLanguage); - // Text dest.writeString(preferredTextLanguage); dest.writeInt(preferredTextRoleFlags); Util.writeBoolean(dest, selectUndeterminedTextLanguage); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java index 71afd87b0..0f2748b1a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionUtil.java @@ -16,18 +16,9 @@ package com.google.android.exoplayer2.trackselection; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import com.google.android.exoplayer2.source.chunk.MediaChunkListIterator; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; import com.google.android.exoplayer2.trackselection.TrackSelection.Definition; -import com.google.android.exoplayer2.util.Assertions; -import java.util.Arrays; -import java.util.List; import org.checkerframework.checker.nullness.compatqual.NullableType; /** Track selection related utility methods. */ @@ -106,261 +97,4 @@ public final class TrackSelectionUtil { } return builder.build(); } - - /** - * Returns average bitrate for chunks in bits per second. Chunks are included in average until - * {@code maxDurationMs} or the first unknown length chunk. - * - * @param iterator Iterator for media chunk sequences. - * @param maxDurationUs Maximum duration of chunks to be included in average bitrate, in - * microseconds. - * @return Average bitrate for chunks in bits per second, or {@link Format#NO_VALUE} if there are - * no chunks or the first chunk length is unknown. - */ - public static int getAverageBitrate(MediaChunkIterator iterator, long maxDurationUs) { - long totalDurationUs = 0; - long totalLength = 0; - while (iterator.next()) { - long chunkLength = iterator.getDataSpec().length; - if (chunkLength == C.LENGTH_UNSET) { - break; - } - long chunkDurationUs = iterator.getChunkEndTimeUs() - iterator.getChunkStartTimeUs(); - if (totalDurationUs + chunkDurationUs >= maxDurationUs) { - totalLength += chunkLength * (maxDurationUs - totalDurationUs) / chunkDurationUs; - totalDurationUs = maxDurationUs; - break; - } - totalDurationUs += chunkDurationUs; - totalLength += chunkLength; - } - return totalDurationUs == 0 - ? Format.NO_VALUE - : (int) (totalLength * C.BITS_PER_BYTE * C.MICROS_PER_SECOND / totalDurationUs); - } - - /** - * Returns bitrate values for a set of tracks whose upcoming media chunk iterators and formats are - * given. - * - *

If an average bitrate can't be calculated, an estimation is calculated using average bitrate - * of another track and the ratio of the bitrate values defined in the formats of the two tracks. - * - * @param iterators An array of {@link MediaChunkIterator}s providing information about the - * sequence of upcoming media chunks for each track. - * @param formats The track formats. - * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in - * microseconds. - * @param bitrates If not null, stores bitrate values in this array. - * @return Average bitrate values for the tracks. If for a track, an average bitrate or an - * estimation can't be calculated, {@link Format#NO_VALUE} is set. - * @see #getAverageBitrate(MediaChunkIterator, long) - */ - @VisibleForTesting - /* package */ static int[] getBitratesUsingFutureInfo( - MediaChunkIterator[] iterators, - Format[] formats, - long maxDurationUs, - @Nullable int[] bitrates) { - int trackCount = iterators.length; - Assertions.checkArgument(trackCount == formats.length); - if (trackCount == 0) { - return new int[0]; - } - if (bitrates == null) { - bitrates = new int[trackCount]; - } - if (maxDurationUs == 0) { - Arrays.fill(bitrates, Format.NO_VALUE); - return bitrates; - } - - int[] formatBitrates = new int[trackCount]; - float[] bitrateRatios = new float[trackCount]; - boolean needEstimateBitrate = false; - boolean canEstimateBitrate = false; - for (int i = 0; i < trackCount; i++) { - int bitrate = getAverageBitrate(iterators[i], maxDurationUs); - if (bitrate != Format.NO_VALUE) { - int formatBitrate = formats[i].bitrate; - formatBitrates[i] = formatBitrate; - if (formatBitrate != Format.NO_VALUE) { - bitrateRatios[i] = ((float) bitrate) / formatBitrate; - canEstimateBitrate = true; - } - } else { - needEstimateBitrate = true; - formatBitrates[i] = Format.NO_VALUE; - } - bitrates[i] = bitrate; - } - - if (needEstimateBitrate && canEstimateBitrate) { - estimateBitrates(bitrates, formats, formatBitrates, bitrateRatios); - } - return bitrates; - } - - /** - * Returns bitrate values for a set of tracks whose formats are given, using the given queue of - * already buffered {@link MediaChunk} instances. - * - * @param queue The queue of already buffered {@link MediaChunk} instances. Must not be modified. - * @param formats The track formats. - * @param maxDurationUs Maximum duration of chunks to be included in average bitrate values, in - * microseconds. - * @param bitrates If not null, calculates bitrate values only for indexes set to Format.NO_VALUE - * and stores result in this array. - * @return Bitrate values for the tracks. If for a track, a bitrate value can't be calculated, - * {@link Format#NO_VALUE} is set. - * @see #getBitratesUsingFutureInfo(MediaChunkIterator[], Format[], long, int[]) - */ - @VisibleForTesting - /* package */ static int[] getBitratesUsingPastInfo( - List queue, - Format[] formats, - long maxDurationUs, - @Nullable int[] bitrates) { - if (bitrates == null) { - bitrates = new int[formats.length]; - Arrays.fill(bitrates, Format.NO_VALUE); - } - if (maxDurationUs == 0) { - return bitrates; - } - int queueAverageBitrate = getAverageQueueBitrate(queue, maxDurationUs); - if (queueAverageBitrate == Format.NO_VALUE) { - return bitrates; - } - int queueFormatBitrate = queue.get(queue.size() - 1).trackFormat.bitrate; - if (queueFormatBitrate != Format.NO_VALUE) { - float queueBitrateRatio = ((float) queueAverageBitrate) / queueFormatBitrate; - estimateBitrates( - bitrates, formats, new int[] {queueFormatBitrate}, new float[] {queueBitrateRatio}); - } - return bitrates; - } - - /** - * Returns bitrate values for a set of tracks whose formats are given, using the given upcoming - * media chunk iterators and the queue of already buffered {@link MediaChunk}s. - * - * @param formats The track formats. - * @param queue The queue of already buffered {@link MediaChunk}s. Must not be modified. - * @param maxPastDurationUs Maximum duration of past chunks to be included in average bitrate - * values, in microseconds. - * @param iterators An array of {@link MediaChunkIterator}s providing information about the - * sequence of upcoming media chunks for each track. - * @param maxFutureDurationUs Maximum duration of future chunks to be included in average bitrate - * values, in microseconds. - * @param useFormatBitrateAsLowerBound Whether to return the estimated bitrate only if it's higher - * than the bitrate of the track's format. - * @param bitrates An array into which the bitrate values will be written. If non-null, this array - * is the one that will be returned. - * @return Bitrate values for the tracks. As long as the format of a track has set bitrate, a - * bitrate value is set in the returned array. Otherwise it might be set to {@link - * Format#NO_VALUE}. - */ - public static int[] getBitratesUsingPastAndFutureInfo( - Format[] formats, - List queue, - long maxPastDurationUs, - MediaChunkIterator[] iterators, - long maxFutureDurationUs, - boolean useFormatBitrateAsLowerBound, - @Nullable int[] bitrates) { - bitrates = getBitratesUsingFutureInfo(iterators, formats, maxFutureDurationUs, bitrates); - getBitratesUsingPastInfo(queue, formats, maxPastDurationUs, bitrates); - for (int i = 0; i < bitrates.length; i++) { - int bitrate = bitrates[i]; - if (bitrate == Format.NO_VALUE - || (useFormatBitrateAsLowerBound - && formats[i].bitrate != Format.NO_VALUE - && bitrate < formats[i].bitrate)) { - bitrates[i] = formats[i].bitrate; - } - } - return bitrates; - } - - /** - * Returns an array containing {@link Format#bitrate} values for given each format in order. - * - * @param formats The format array to copy {@link Format#bitrate} values. - * @param bitrates If not null, stores bitrate values in this array. - * @return An array containing {@link Format#bitrate} values for given each format in order. - */ - public static int[] getFormatBitrates(Format[] formats, @Nullable int[] bitrates) { - int trackCount = formats.length; - if (bitrates == null) { - bitrates = new int[trackCount]; - } - for (int i = 0; i < trackCount; i++) { - bitrates[i] = formats[i].bitrate; - } - return bitrates; - } - - /** - * Fills missing values in the given {@code bitrates} array by calculates an estimation using the - * closest reference bitrate value. - * - * @param bitrates An array of bitrates to be filled with estimations. Missing values are set to - * {@link Format#NO_VALUE}. - * @param formats An array of formats, one for each bitrate. - * @param referenceBitrates An array of reference bitrates which are used to calculate - * estimations. - * @param referenceBitrateRatios An array containing ratio of reference bitrates to their bitrate - * estimates. - */ - private static void estimateBitrates( - int[] bitrates, Format[] formats, int[] referenceBitrates, float[] referenceBitrateRatios) { - for (int i = 0; i < bitrates.length; i++) { - if (bitrates[i] == Format.NO_VALUE) { - int formatBitrate = formats[i].bitrate; - if (formatBitrate != Format.NO_VALUE) { - int closestReferenceBitrateIndex = - getClosestBitrateIndex(formatBitrate, referenceBitrates); - bitrates[i] = - (int) (referenceBitrateRatios[closestReferenceBitrateIndex] * formatBitrate); - } - } - } - } - - private static int getAverageQueueBitrate(List queue, long maxDurationUs) { - if (queue.isEmpty()) { - return Format.NO_VALUE; - } - MediaChunkListIterator iterator = - new MediaChunkListIterator(getSingleFormatSubQueue(queue), /* reverseOrder= */ true); - return getAverageBitrate(iterator, maxDurationUs); - } - - private static List getSingleFormatSubQueue( - List queue) { - Format queueFormat = queue.get(queue.size() - 1).trackFormat; - int queueSize = queue.size(); - for (int i = queueSize - 2; i >= 0; i--) { - if (!queue.get(i).trackFormat.equals(queueFormat)) { - return queue.subList(i + 1, queueSize); - } - } - return queue; - } - - private static int getClosestBitrateIndex(int formatBitrate, int[] formatBitrates) { - int closestDistance = Integer.MAX_VALUE; - int closestFormat = C.INDEX_UNSET; - for (int j = 0; j < formatBitrates.length; j++) { - if (formatBitrates[j] != Format.NO_VALUE) { - int distance = Math.abs(formatBitrates[j] - formatBitrate); - if (distance < closestDistance) { - closestDistance = distance; - closestFormat = j; - } - } - } - return closestFormat; - } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index f2fbd8911..d48c140ac 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -36,8 +36,6 @@ import com.google.android.exoplayer2.util.Assertions; * * The following interactions occur between the player and its track selector during playback. * - *

- * *

    *
  • When the player is created it will initialize the track selector by calling {@link * #init(InvalidationListener, BandwidthMeter)}. @@ -75,7 +73,7 @@ import com.google.android.exoplayer2.util.Assertions; * the two are tightly bound together. It may only be possible to play a certain combination tracks * if the renderers are configured in a particular way. Equally, it may only be possible to * configure renderers in a particular way if certain tracks are selected. Hence it makes sense to - * determined the track selection and corresponding renderer configurations in a single step. + * determine the track selection and corresponding renderer configurations in a single step. * *

    Threading model

    * @@ -98,8 +96,8 @@ public abstract class TrackSelector { } - private @Nullable InvalidationListener listener; - private @Nullable BandwidthMeter bandwidthMeter; + @Nullable private InvalidationListener listener; + @Nullable private BandwidthMeter bandwidthMeter; /** * Called by the player to initialize the selector. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index fc723134f..9228f3af6 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -85,8 +85,8 @@ public final class TrackSelectorResult { /** * Returns whether this result is equivalent to {@code other} for the renderer at the given index. - * The results are equivalent if they have equal renderersEnabled array, track selections, and - * configurations for the renderer. + * The results are equivalent if they have equal track selections and configurations for the + * renderer. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} * will be returned. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java deleted file mode 100644 index 25f7e4ea7..000000000 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/WindowedTrackBitrateEstimator.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.exoplayer2.trackselection; - -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunkIterator; -import java.util.List; - -/** A {@link TrackBitrateEstimator} which derives estimates from a window of time. */ -public final class WindowedTrackBitrateEstimator implements TrackBitrateEstimator { - - private final long maxPastDurationUs; - private final long maxFutureDurationUs; - private final boolean useFormatBitrateAsLowerBound; - - /** - * @param maxPastDurationMs Maximum duration of past chunks to be included in average bitrate - * values, in milliseconds. - * @param maxFutureDurationMs Maximum duration of future chunks to be included in average bitrate - * values, in milliseconds. - * @param useFormatBitrateAsLowerBound Whether to use the bitrate of the track's format as a lower - * bound for the estimated bitrate. - */ - public WindowedTrackBitrateEstimator( - long maxPastDurationMs, long maxFutureDurationMs, boolean useFormatBitrateAsLowerBound) { - this.maxPastDurationUs = C.msToUs(maxPastDurationMs); - this.maxFutureDurationUs = C.msToUs(maxFutureDurationMs); - this.useFormatBitrateAsLowerBound = useFormatBitrateAsLowerBound; - } - - @Override - public int[] getBitrates( - Format[] formats, - List queue, - MediaChunkIterator[] iterators, - @Nullable int[] bitrates) { - if (maxFutureDurationUs > 0 || maxPastDurationUs > 0) { - return TrackSelectionUtil.getBitratesUsingPastAndFutureInfo( - formats, - queue, - maxPastDurationUs, - iterators, - maxFutureDurationUs, - useFormatBitrateAsLowerBound, - bitrates); - } - return TrackSelectionUtil.getFormatBitrates(formats, bitrates); - } -} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/package-info.java new file mode 100644 index 000000000..45131e644 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/trackselection/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.trackselection; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java index 9224e14d4..3c92b039c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/AssetDataSource.java @@ -15,11 +15,14 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.content.res.AssetManager; import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -40,8 +43,8 @@ public final class AssetDataSource extends BaseDataSource { private final AssetManager assetManager; - private @Nullable Uri uri; - private @Nullable InputStream inputStream; + @Nullable private Uri uri; + @Nullable private InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -51,25 +54,11 @@ public final class AssetDataSource extends BaseDataSource { this.assetManager = context.getAssets(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #AssetDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public AssetDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws AssetDataSourceException { try { uri = dataSpec.uri; - String path = uri.getPath(); + String path = Assertions.checkNotNull(uri.getPath()); if (path.startsWith("/android_asset/")) { path = path.substring(15); } else if (path.startsWith("/")) { @@ -115,7 +104,7 @@ public final class AssetDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new AssetDataSourceException(e); } @@ -135,7 +124,8 @@ public final class AssetDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java index 21f2d5993..80687db31 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/BaseDataSource.java @@ -33,7 +33,7 @@ public abstract class BaseDataSource implements DataSource { private final ArrayList listeners; private int listenerCount; - private @Nullable DataSpec dataSpec; + @Nullable private DataSpec dataSpec; /** * Creates base data source. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java index 4017c1f02..2ba6ab4c6 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java @@ -15,20 +15,24 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * A {@link DataSink} for writing to a byte array. */ public final class ByteArrayDataSink implements DataSink { - private ByteArrayOutputStream stream; + private @MonotonicNonNull ByteArrayOutputStream stream; @Override - public void open(DataSpec dataSpec) throws IOException { + public void open(DataSpec dataSpec) { if (dataSpec.length == C.LENGTH_UNSET) { stream = new ByteArrayOutputStream(); } else { @@ -39,18 +43,19 @@ public final class ByteArrayDataSink implements DataSink { @Override public void close() throws IOException { - stream.close(); + castNonNull(stream).close(); } @Override - public void write(byte[] buffer, int offset, int length) throws IOException { - stream.write(buffer, offset, length); + public void write(byte[] buffer, int offset, int length) { + castNonNull(stream).write(buffer, offset, length); } /** * Returns the data written to the sink since the last call to {@link #open(DataSpec)}, or null if * {@link #open(DataSpec)} has never been called. */ + @Nullable public byte[] getData() { return stream == null ? null : stream.toByteArray(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java index c45089667..ed5ba9064 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java @@ -26,7 +26,7 @@ public final class ByteArrayDataSource extends BaseDataSource { private final byte[] data; - private @Nullable Uri uri; + @Nullable private Uri uri; private int readPosition; private int bytesRemaining; private boolean opened; @@ -58,7 +58,7 @@ public final class ByteArrayDataSource extends BaseDataSource { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(byte[] buffer, int offset, int readLength) { if (readLength == 0) { return 0; } else if (bytesRemaining == 0) { @@ -74,12 +74,13 @@ public final class ByteArrayDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } @Override - public void close() throws IOException { + public void close() { if (opened) { opened = false; transferEnded(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index c723d3f1c..baaa67712 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; @@ -43,9 +45,9 @@ public final class ContentDataSource extends BaseDataSource { private final ContentResolver resolver; - private @Nullable Uri uri; - private @Nullable AssetFileDescriptor assetFileDescriptor; - private @Nullable FileInputStream inputStream; + @Nullable private Uri uri; + @Nullable private AssetFileDescriptor assetFileDescriptor; + @Nullable private FileInputStream inputStream; private long bytesRemaining; private boolean opened; @@ -57,30 +59,21 @@ public final class ContentDataSource extends BaseDataSource { this.resolver = context.getContentResolver(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #ContentDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public ContentDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws ContentDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; + transferInitializing(dataSpec); - assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + AssetFileDescriptor assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + this.assetFileDescriptor = assetFileDescriptor; if (assetFileDescriptor == null) { throw new FileNotFoundException("Could not open file descriptor for: " + uri); } - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + long assetStartOffset = assetFileDescriptor.getStartOffset(); long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset; if (skipped != dataSpec.position) { @@ -124,7 +117,7 @@ public final class ContentDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new ContentDataSourceException(e); } @@ -144,7 +137,8 @@ public final class ContentDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java index 94a6e21c8..e592c3bec 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -18,8 +18,8 @@ package com.google.android.exoplayer2.upstream; import static com.google.android.exoplayer2.util.Util.castNonNull; import android.net.Uri; -import androidx.annotation.Nullable; import android.util.Base64; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.Util; @@ -36,6 +36,8 @@ public final class DataSchemeDataSource extends BaseDataSource { private int endPosition; private int readPosition; + // the constructor does not initialize fields: data + @SuppressWarnings("nullness:initialization.fields.uninitialized") public DataSchemeDataSource() { super(/* isNetwork= */ false); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index 78b31896a..acf555042 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -24,6 +24,9 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; /** * Defines a region of data. @@ -32,19 +35,14 @@ public final class DataSpec { /** * The flags that apply to any request for data. Possible flag values are {@link - * #FLAG_ALLOW_GZIP}, {@link #FLAG_ALLOW_ICY_METADATA}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} - * and {@link #FLAG_ALLOW_CACHE_FRAGMENTATION}. + * #FLAG_ALLOW_GZIP}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} and {@link + * #FLAG_ALLOW_CACHE_FRAGMENTATION}. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, - value = { - FLAG_ALLOW_GZIP, - FLAG_ALLOW_ICY_METADATA, - FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, - FLAG_ALLOW_CACHE_FRAGMENTATION - }) + value = {FLAG_ALLOW_GZIP, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, FLAG_ALLOW_CACHE_FRAGMENTATION}) public @interface Flags {} /** * Allows an underlying network stack to request that the server use gzip compression. @@ -58,17 +56,15 @@ public final class DataSpec { * DataSource#read(byte[], int, int)} will be the decompressed data. */ public static final int FLAG_ALLOW_GZIP = 1; - /** Allows an underlying network stack to request that the stream contain ICY metadata. */ - public static final int FLAG_ALLOW_ICY_METADATA = 1 << 1; // 2 /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */ - public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 2; // 4 + public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; // 2 /** * Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy * will be able to evict individual fragments of the data. Depending on the cache implementation, * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment * whilst writing another). */ - public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 3; // 8 + public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 2; // 4 /** * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link @@ -95,16 +91,15 @@ public final class DataSpec { public final @HttpMethod int httpMethod; /** - * The HTTP body, null otherwise. If the body is non-null, then httpBody.length will be non-zero. + * The HTTP request body, null otherwise. If the body is non-null, then httpBody.length will be + * non-zero. */ - public final @Nullable byte[] httpBody; + @Nullable public final byte[] httpBody; - /** @deprecated Use {@link #httpBody} instead. */ - @Deprecated public final @Nullable byte[] postBody; + /** Immutable map containing the headers to use in HTTP requests. */ + public final Map httpRequestHeaders; - /** - * The absolute position of the data in the full stream. - */ + /** The absolute position of the data in the full stream. */ public final long absoluteStreamPosition; /** * The position of the data when read from {@link #uri}. @@ -121,7 +116,7 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * data spec is not intended to be used in conjunction with a cache. */ - public final @Nullable String key; + @Nullable public final String key; /** Request {@link Flags flags}. */ public final @Flags int flags; @@ -170,6 +165,36 @@ public final class DataSpec { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); } + /** + * Construct a data spec where {@link #position} equals {@link #absoluteStreamPosition} and has + * request headers. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders} + */ + public DataSpec( + Uri uri, + long absoluteStreamPosition, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { + this( + uri, + inferHttpMethod(null), + null, + absoluteStreamPosition, + absoluteStreamPosition, + length, + key, + flags, + httpRequestHeaders); + } + /** * Construct a data spec where {@link #position} may differ from {@link #absoluteStreamPosition}. * @@ -214,7 +239,7 @@ public final class DataSpec { @Flags int flags) { this( uri, - /* httpMethod= */ postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET, + /* httpMethod= */ inferHttpMethod(postBody), /* httpBody= */ postBody, absoluteStreamPosition, position, @@ -235,7 +260,6 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - @SuppressWarnings("deprecation") public DataSpec( Uri uri, @HttpMethod int httpMethod, @@ -245,18 +269,53 @@ public final class DataSpec { long length, @Nullable String key, @Flags int flags) { + this( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + /* httpRequestHeaders= */ Collections.emptyMap()); + } + + /** + * Construct a data spec with request parameters to be used as HTTP headers inside HTTP requests. + * + * @param uri {@link #uri}. + * @param httpMethod {@link #httpMethod}. + * @param httpBody {@link #httpBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + * @param httpRequestHeaders {@link #httpRequestHeaders}. + */ + public DataSpec( + Uri uri, + @HttpMethod int httpMethod, + @Nullable byte[] httpBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags, + Map httpRequestHeaders) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); this.uri = uri; this.httpMethod = httpMethod; this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null; - this.postBody = this.httpBody; this.absoluteStreamPosition = absoluteStreamPosition; this.position = position; this.length = length; this.key = key; this.flags = flags; + this.httpRequestHeaders = Collections.unmodifiableMap(new HashMap<>(httpRequestHeaders)); } /** @@ -344,7 +403,8 @@ public final class DataSpec { position + offset, length, key, - flags); + flags, + httpRequestHeaders); } } @@ -356,6 +416,63 @@ public final class DataSpec { */ public DataSpec withUri(Uri uri) { return new DataSpec( - uri, httpMethod, httpBody, absoluteStreamPosition, position, length, key, flags); + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + httpRequestHeaders); + } + + /** + * Returns a copy of this data spec with the specified request headers. + * + * @param requestHeaders The HTTP request headers. + * @return The copied data spec with the specified request headers. + */ + public DataSpec withRequestHeaders(Map requestHeaders) { + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + requestHeaders); + } + + /** + * Returns a copy this data spec with additional request headers. + * + *

    Note: Values in {@code requestHeaders} will overwrite values with the same header key that + * were previously set in this instance's {@code #httpRequestHeaders}. + * + * @param requestHeaders The additional HTTP request headers. + * @return The copied data with the additional HTTP request headers. + */ + public DataSpec withAdditionalHeaders(Map requestHeaders) { + Map totalHeaders = new HashMap<>(this.httpRequestHeaders); + totalHeaders.putAll(requestHeaders); + + return new DataSpec( + uri, + httpMethod, + httpBody, + absoluteStreamPosition, + position, + length, + key, + flags, + totalHeaders); + } + + @HttpMethod + private static int inferHttpMethod(@Nullable byte[] postBody) { + return postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET; } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java index 1f306dd69..6cbc17d3e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java @@ -22,8 +22,8 @@ import android.content.IntentFilter; import android.net.ConnectivityManager; import android.os.Handler; import android.os.Looper; -import androidx.annotation.Nullable; import android.util.SparseArray; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; @@ -56,19 +56,19 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Default initial Wifi bitrate estimate in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI = - new long[] {5_400_000, 3_400_000, 1_900_000, 1_100_000, 400_000}; + new long[] {5_800_000, 3_500_000, 1_900_000, 1_000_000, 520_000}; /** Default initial 2G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_2G = - new long[] {170_000, 139_000, 122_000, 107_000, 90_000}; + new long[] {204_000, 154_000, 139_000, 122_000, 102_000}; /** Default initial 3G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_3G = - new long[] {2_100_000, 1_300_000, 960_000, 770_000, 450_000}; + new long[] {2_200_000, 1_150_000, 810_000, 640_000, 450_000}; /** Default initial 4G bitrate estimates in bits per second. */ public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_4G = - new long[] {6_000_000, 3_400_000, 2_100_000, 1_400_000, 570_000}; + new long[] {4_900_000, 2_300_000, 1_500_000, 970_000, 540_000}; /** * Default initial bitrate estimate used when the device is offline or the network type cannot be @@ -79,6 +79,8 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList /** Default maximum weight for the sliding window. */ public static final int DEFAULT_SLIDING_WINDOW_MAX_WEIGHT = 2000; + @Nullable private static DefaultBandwidthMeter singletonInstance; + /** Builder for a bandwidth meter. */ public static final class Builder { @@ -201,9 +203,11 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList result.append(C.NETWORK_TYPE_2G, DEFAULT_INITIAL_BITRATE_ESTIMATES_2G[groupIndices[1]]); result.append(C.NETWORK_TYPE_3G, DEFAULT_INITIAL_BITRATE_ESTIMATES_3G[groupIndices[2]]); result.append(C.NETWORK_TYPE_4G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); - // Assume default Wifi bitrate for Ethernet to prevent using the slower fallback bitrate. + // Assume default Wifi and 4G bitrate for Ethernet and 5G, respectively, to prevent using the + // slower fallback. result.append( C.NETWORK_TYPE_ETHERNET, DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI[groupIndices[0]]); + result.append(C.NETWORK_TYPE_5G, DEFAULT_INITIAL_BITRATE_ESTIMATES_4G[groupIndices[3]]); return result; } @@ -214,6 +218,19 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList } } + /** + * Returns a singleton instance of a {@link DefaultBandwidthMeter} with default configuration. + * + * @param context A {@link Context}. + * @return The singleton instance. + */ + public static synchronized DefaultBandwidthMeter getSingletonInstance(Context context) { + if (singletonInstance == null) { + singletonInstance = new DefaultBandwidthMeter.Builder(context).build(); + } + return singletonInstance; + } + private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000; private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024; @@ -341,7 +358,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList totalElapsedTimeMs += sampleElapsedTimeMs; totalBytesTransferred += sampleBytesTransferred; if (sampleElapsedTimeMs > 0) { - float bitsPerSecond = (sampleBytesTransferred * 8000) / sampleElapsedTimeMs; + float bitsPerSecond = (sampleBytesTransferred * 8000f) / sampleElapsedTimeMs; slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond); if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) { @@ -412,7 +429,7 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList */ private static class ConnectivityActionReceiver extends BroadcastReceiver { - @MonotonicNonNull private static ConnectivityActionReceiver staticInstance; + private static @MonotonicNonNull ConnectivityActionReceiver staticInstance; private final Handler mainHandler; private final ArrayList> bandwidthMeters; @@ -472,247 +489,245 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList private static Map createInitialBitrateCountryGroupAssignment() { HashMap countryGroupAssignment = new HashMap<>(); - countryGroupAssignment.put("AD", new int[] {1, 0, 0, 1}); - countryGroupAssignment.put("AE", new int[] {1, 4, 4, 4}); + countryGroupAssignment.put("AD", new int[] {0, 2, 0, 0}); + countryGroupAssignment.put("AE", new int[] {2, 4, 4, 4}); countryGroupAssignment.put("AF", new int[] {4, 4, 3, 3}); - countryGroupAssignment.put("AG", new int[] {3, 2, 1, 1}); - countryGroupAssignment.put("AI", new int[] {1, 0, 1, 3}); - countryGroupAssignment.put("AL", new int[] {1, 2, 1, 1}); - countryGroupAssignment.put("AM", new int[] {2, 2, 3, 2}); - countryGroupAssignment.put("AO", new int[] {3, 4, 2, 0}); + countryGroupAssignment.put("AG", new int[] {4, 2, 2, 3}); + countryGroupAssignment.put("AI", new int[] {0, 3, 2, 4}); + countryGroupAssignment.put("AL", new int[] {1, 2, 0, 1}); + countryGroupAssignment.put("AM", new int[] {2, 2, 1, 2}); + countryGroupAssignment.put("AO", new int[] {3, 4, 3, 1}); countryGroupAssignment.put("AQ", new int[] {4, 2, 2, 2}); - countryGroupAssignment.put("AR", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("AS", new int[] {3, 3, 4, 1}); - countryGroupAssignment.put("AT", new int[] {0, 2, 0, 0}); - countryGroupAssignment.put("AU", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("AW", new int[] {1, 1, 0, 2}); - countryGroupAssignment.put("AX", new int[] {0, 2, 1, 0}); - countryGroupAssignment.put("AZ", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("BA", new int[] {1, 1, 1, 2}); - countryGroupAssignment.put("BB", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("BD", new int[] {2, 2, 3, 2}); - countryGroupAssignment.put("BE", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("BF", new int[] {4, 4, 3, 1}); + countryGroupAssignment.put("AR", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("AS", new int[] {2, 2, 4, 2}); + countryGroupAssignment.put("AT", new int[] {0, 3, 0, 0}); + countryGroupAssignment.put("AU", new int[] {0, 2, 0, 1}); + countryGroupAssignment.put("AW", new int[] {1, 1, 2, 4}); + countryGroupAssignment.put("AX", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("AZ", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("BA", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("BB", new int[] {0, 3, 0, 0}); + countryGroupAssignment.put("BD", new int[] {2, 0, 4, 3}); + countryGroupAssignment.put("BE", new int[] {0, 1, 2, 3}); + countryGroupAssignment.put("BF", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("BG", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("BH", new int[] {2, 1, 3, 4}); - countryGroupAssignment.put("BI", new int[] {4, 3, 4, 4}); - countryGroupAssignment.put("BJ", new int[] {4, 3, 4, 4}); - countryGroupAssignment.put("BL", new int[] {1, 0, 2, 3}); - countryGroupAssignment.put("BM", new int[] {1, 0, 0, 0}); - countryGroupAssignment.put("BN", new int[] {4, 2, 3, 3}); - countryGroupAssignment.put("BO", new int[] {2, 2, 3, 2}); - countryGroupAssignment.put("BQ", new int[] {1, 0, 3, 4}); - countryGroupAssignment.put("BR", new int[] {2, 3, 3, 2}); - countryGroupAssignment.put("BS", new int[] {2, 0, 1, 4}); - countryGroupAssignment.put("BT", new int[] {3, 0, 2, 1}); - countryGroupAssignment.put("BW", new int[] {4, 4, 1, 2}); - countryGroupAssignment.put("BY", new int[] {0, 1, 1, 2}); - countryGroupAssignment.put("BZ", new int[] {2, 2, 3, 1}); - countryGroupAssignment.put("CA", new int[] {0, 3, 3, 3}); - countryGroupAssignment.put("CD", new int[] {4, 4, 3, 2}); - countryGroupAssignment.put("CF", new int[] {4, 3, 3, 4}); - countryGroupAssignment.put("CG", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("CH", new int[] {0, 0, 1, 1}); + countryGroupAssignment.put("BH", new int[] {1, 0, 3, 4}); + countryGroupAssignment.put("BI", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("BJ", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("BL", new int[] {1, 0, 4, 3}); + countryGroupAssignment.put("BM", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("BN", new int[] {4, 0, 2, 4}); + countryGroupAssignment.put("BO", new int[] {1, 3, 3, 3}); + countryGroupAssignment.put("BQ", new int[] {1, 0, 1, 0}); + countryGroupAssignment.put("BR", new int[] {2, 4, 3, 1}); + countryGroupAssignment.put("BS", new int[] {3, 1, 1, 3}); + countryGroupAssignment.put("BT", new int[] {3, 0, 3, 1}); + countryGroupAssignment.put("BW", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("BY", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("BZ", new int[] {1, 3, 2, 1}); + countryGroupAssignment.put("CA", new int[] {0, 3, 2, 2}); + countryGroupAssignment.put("CD", new int[] {3, 4, 2, 2}); + countryGroupAssignment.put("CF", new int[] {4, 3, 2, 2}); + countryGroupAssignment.put("CG", new int[] {3, 4, 1, 1}); + countryGroupAssignment.put("CH", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("CI", new int[] {3, 4, 3, 3}); - countryGroupAssignment.put("CK", new int[] {2, 4, 1, 0}); - countryGroupAssignment.put("CL", new int[] {2, 2, 2, 3}); - countryGroupAssignment.put("CM", new int[] {3, 4, 2, 1}); - countryGroupAssignment.put("CN", new int[] {2, 2, 2, 3}); - countryGroupAssignment.put("CO", new int[] {2, 3, 2, 2}); + countryGroupAssignment.put("CK", new int[] {2, 0, 1, 0}); + countryGroupAssignment.put("CL", new int[] {1, 2, 2, 3}); + countryGroupAssignment.put("CM", new int[] {3, 4, 3, 2}); + countryGroupAssignment.put("CN", new int[] {1, 0, 1, 1}); + countryGroupAssignment.put("CO", new int[] {2, 3, 3, 2}); countryGroupAssignment.put("CR", new int[] {2, 2, 4, 4}); - countryGroupAssignment.put("CU", new int[] {4, 4, 3, 1}); - countryGroupAssignment.put("CV", new int[] {2, 3, 2, 4}); - countryGroupAssignment.put("CW", new int[] {1, 0, 0, 0}); - countryGroupAssignment.put("CX", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("CY", new int[] {1, 1, 1, 1}); + countryGroupAssignment.put("CU", new int[] {4, 4, 2, 1}); + countryGroupAssignment.put("CV", new int[] {2, 3, 3, 2}); + countryGroupAssignment.put("CW", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("CY", new int[] {1, 1, 0, 0}); countryGroupAssignment.put("CZ", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("DE", new int[] {0, 2, 2, 2}); - countryGroupAssignment.put("DJ", new int[] {3, 3, 4, 0}); - countryGroupAssignment.put("DK", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("DM", new int[] {1, 0, 0, 3}); + countryGroupAssignment.put("DE", new int[] {0, 1, 2, 3}); + countryGroupAssignment.put("DJ", new int[] {4, 2, 4, 4}); + countryGroupAssignment.put("DK", new int[] {0, 0, 1, 0}); + countryGroupAssignment.put("DM", new int[] {1, 1, 0, 2}); countryGroupAssignment.put("DO", new int[] {3, 3, 4, 4}); countryGroupAssignment.put("DZ", new int[] {3, 3, 4, 4}); - countryGroupAssignment.put("EC", new int[] {2, 4, 4, 2}); + countryGroupAssignment.put("EC", new int[] {2, 3, 4, 2}); countryGroupAssignment.put("EE", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("EG", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("EH", new int[] {2, 0, 3, 3}); - countryGroupAssignment.put("ER", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("EG", new int[] {3, 4, 2, 1}); + countryGroupAssignment.put("EH", new int[] {2, 0, 3, 1}); + countryGroupAssignment.put("ER", new int[] {4, 2, 4, 4}); countryGroupAssignment.put("ES", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("ET", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("ET", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("FI", new int[] {0, 0, 1, 0}); - countryGroupAssignment.put("FJ", new int[] {3, 1, 3, 3}); - countryGroupAssignment.put("FK", new int[] {4, 2, 2, 3}); - countryGroupAssignment.put("FM", new int[] {4, 2, 4, 0}); - countryGroupAssignment.put("FO", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("FR", new int[] {1, 0, 3, 1}); - countryGroupAssignment.put("GA", new int[] {3, 3, 2, 1}); - countryGroupAssignment.put("GB", new int[] {0, 1, 3, 3}); - countryGroupAssignment.put("GD", new int[] {2, 0, 4, 4}); - countryGroupAssignment.put("GE", new int[] {1, 1, 0, 3}); - countryGroupAssignment.put("GF", new int[] {1, 2, 4, 4}); - countryGroupAssignment.put("GG", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("GH", new int[] {3, 3, 3, 2}); - countryGroupAssignment.put("GI", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("GL", new int[] {2, 2, 3, 4}); - countryGroupAssignment.put("GM", new int[] {4, 3, 3, 2}); - countryGroupAssignment.put("GN", new int[] {4, 4, 4, 0}); - countryGroupAssignment.put("GP", new int[] {2, 2, 1, 3}); - countryGroupAssignment.put("GQ", new int[] {4, 3, 3, 0}); + countryGroupAssignment.put("FJ", new int[] {3, 0, 4, 4}); + countryGroupAssignment.put("FK", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("FM", new int[] {3, 2, 4, 1}); + countryGroupAssignment.put("FO", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("FR", new int[] {1, 1, 1, 1}); + countryGroupAssignment.put("GA", new int[] {3, 2, 2, 2}); + countryGroupAssignment.put("GB", new int[] {0, 1, 1, 1}); + countryGroupAssignment.put("GD", new int[] {1, 1, 3, 1}); + countryGroupAssignment.put("GE", new int[] {1, 0, 1, 4}); + countryGroupAssignment.put("GF", new int[] {2, 0, 1, 3}); + countryGroupAssignment.put("GG", new int[] {1, 0, 0, 0}); + countryGroupAssignment.put("GH", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("GI", new int[] {4, 4, 0, 0}); + countryGroupAssignment.put("GL", new int[] {2, 1, 1, 2}); + countryGroupAssignment.put("GM", new int[] {4, 3, 2, 4}); + countryGroupAssignment.put("GN", new int[] {3, 4, 4, 2}); + countryGroupAssignment.put("GP", new int[] {2, 1, 3, 4}); + countryGroupAssignment.put("GQ", new int[] {4, 4, 4, 0}); countryGroupAssignment.put("GR", new int[] {1, 1, 0, 1}); - countryGroupAssignment.put("GT", new int[] {3, 3, 3, 4}); - countryGroupAssignment.put("GU", new int[] {1, 2, 4, 4}); - countryGroupAssignment.put("GW", new int[] {4, 4, 4, 0}); - countryGroupAssignment.put("GY", new int[] {3, 4, 1, 0}); - countryGroupAssignment.put("HK", new int[] {0, 1, 4, 4}); - countryGroupAssignment.put("HN", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("HR", new int[] {1, 0, 0, 2}); - countryGroupAssignment.put("HT", new int[] {3, 4, 4, 3}); - countryGroupAssignment.put("HU", new int[] {0, 0, 1, 0}); - countryGroupAssignment.put("ID", new int[] {3, 2, 3, 4}); - countryGroupAssignment.put("IE", new int[] {0, 0, 3, 2}); - countryGroupAssignment.put("IL", new int[] {0, 1, 2, 3}); + countryGroupAssignment.put("GT", new int[] {3, 2, 2, 2}); + countryGroupAssignment.put("GU", new int[] {1, 0, 2, 2}); + countryGroupAssignment.put("GW", new int[] {3, 4, 4, 3}); + countryGroupAssignment.put("GY", new int[] {3, 2, 1, 1}); + countryGroupAssignment.put("HK", new int[] {0, 2, 3, 4}); + countryGroupAssignment.put("HN", new int[] {3, 1, 3, 3}); + countryGroupAssignment.put("HR", new int[] {1, 1, 0, 1}); + countryGroupAssignment.put("HT", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("HU", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("ID", new int[] {2, 2, 2, 3}); + countryGroupAssignment.put("IE", new int[] {1, 0, 1, 1}); + countryGroupAssignment.put("IL", new int[] {1, 0, 2, 3}); countryGroupAssignment.put("IM", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("IN", new int[] {2, 2, 4, 4}); - countryGroupAssignment.put("IO", new int[] {4, 4, 2, 2}); - countryGroupAssignment.put("IQ", new int[] {3, 3, 4, 4}); - countryGroupAssignment.put("IR", new int[] {1, 0, 1, 0}); - countryGroupAssignment.put("IS", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("IT", new int[] {1, 0, 1, 1}); + countryGroupAssignment.put("IN", new int[] {2, 2, 4, 3}); + countryGroupAssignment.put("IO", new int[] {4, 4, 2, 3}); + countryGroupAssignment.put("IQ", new int[] {3, 3, 4, 2}); + countryGroupAssignment.put("IR", new int[] {3, 0, 2, 1}); + countryGroupAssignment.put("IS", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("IT", new int[] {1, 1, 1, 2}); countryGroupAssignment.put("JE", new int[] {1, 0, 0, 1}); - countryGroupAssignment.put("JM", new int[] {3, 2, 2, 1}); - countryGroupAssignment.put("JO", new int[] {1, 1, 1, 2}); - countryGroupAssignment.put("JP", new int[] {0, 2, 2, 2}); - countryGroupAssignment.put("KE", new int[] {3, 3, 3, 3}); - countryGroupAssignment.put("KG", new int[] {1, 1, 2, 3}); - countryGroupAssignment.put("KH", new int[] {2, 0, 4, 4}); - countryGroupAssignment.put("KI", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("KM", new int[] {4, 4, 3, 3}); - countryGroupAssignment.put("KN", new int[] {1, 0, 1, 4}); - countryGroupAssignment.put("KP", new int[] {1, 2, 0, 2}); - countryGroupAssignment.put("KR", new int[] {0, 3, 0, 2}); - countryGroupAssignment.put("KW", new int[] {2, 2, 1, 2}); - countryGroupAssignment.put("KY", new int[] {1, 1, 0, 2}); + countryGroupAssignment.put("JM", new int[] {3, 3, 3, 4}); + countryGroupAssignment.put("JO", new int[] {1, 2, 1, 1}); + countryGroupAssignment.put("JP", new int[] {0, 2, 0, 0}); + countryGroupAssignment.put("KE", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("KG", new int[] {2, 0, 2, 2}); + countryGroupAssignment.put("KH", new int[] {1, 0, 4, 3}); + countryGroupAssignment.put("KI", new int[] {4, 4, 4, 0}); + countryGroupAssignment.put("KM", new int[] {4, 3, 2, 4}); + countryGroupAssignment.put("KN", new int[] {1, 0, 2, 4}); + countryGroupAssignment.put("KP", new int[] {4, 2, 0, 2}); + countryGroupAssignment.put("KR", new int[] {0, 1, 0, 1}); + countryGroupAssignment.put("KW", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("KY", new int[] {3, 1, 2, 3}); countryGroupAssignment.put("KZ", new int[] {1, 2, 2, 2}); - countryGroupAssignment.put("LA", new int[] {2, 1, 1, 0}); + countryGroupAssignment.put("LA", new int[] {2, 2, 1, 1}); countryGroupAssignment.put("LB", new int[] {3, 2, 0, 0}); - countryGroupAssignment.put("LC", new int[] {2, 1, 0, 0}); - countryGroupAssignment.put("LI", new int[] {0, 0, 2, 2}); - countryGroupAssignment.put("LK", new int[] {1, 1, 2, 2}); - countryGroupAssignment.put("LR", new int[] {3, 4, 4, 1}); - countryGroupAssignment.put("LS", new int[] {3, 3, 2, 0}); + countryGroupAssignment.put("LC", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("LI", new int[] {0, 0, 1, 1}); + countryGroupAssignment.put("LK", new int[] {2, 0, 2, 3}); + countryGroupAssignment.put("LR", new int[] {3, 4, 4, 2}); + countryGroupAssignment.put("LS", new int[] {3, 3, 2, 2}); countryGroupAssignment.put("LT", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("LU", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("LU", new int[] {0, 0, 0, 0}); countryGroupAssignment.put("LV", new int[] {0, 0, 0, 0}); - countryGroupAssignment.put("LY", new int[] {4, 3, 4, 4}); - countryGroupAssignment.put("MA", new int[] {2, 1, 2, 2}); - countryGroupAssignment.put("MC", new int[] {1, 0, 1, 0}); + countryGroupAssignment.put("LY", new int[] {3, 3, 4, 3}); + countryGroupAssignment.put("MA", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("MC", new int[] {0, 4, 0, 0}); countryGroupAssignment.put("MD", new int[] {1, 1, 0, 0}); - countryGroupAssignment.put("ME", new int[] {1, 2, 2, 3}); - countryGroupAssignment.put("MF", new int[] {1, 4, 2, 1}); - countryGroupAssignment.put("MG", new int[] {3, 4, 1, 3}); - countryGroupAssignment.put("MH", new int[] {4, 0, 2, 3}); + countryGroupAssignment.put("ME", new int[] {1, 3, 1, 2}); + countryGroupAssignment.put("MF", new int[] {2, 3, 1, 1}); + countryGroupAssignment.put("MG", new int[] {3, 4, 2, 3}); + countryGroupAssignment.put("MH", new int[] {4, 0, 2, 4}); countryGroupAssignment.put("MK", new int[] {1, 0, 0, 0}); - countryGroupAssignment.put("ML", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("MM", new int[] {2, 3, 1, 2}); - countryGroupAssignment.put("MN", new int[] {2, 3, 2, 4}); + countryGroupAssignment.put("ML", new int[] {4, 4, 2, 0}); + countryGroupAssignment.put("MM", new int[] {3, 3, 2, 2}); + countryGroupAssignment.put("MN", new int[] {2, 3, 1, 1}); countryGroupAssignment.put("MO", new int[] {0, 0, 4, 4}); - countryGroupAssignment.put("MP", new int[] {0, 2, 4, 4}); - countryGroupAssignment.put("MQ", new int[] {1, 1, 1, 3}); - countryGroupAssignment.put("MR", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("MS", new int[] {1, 4, 0, 3}); - countryGroupAssignment.put("MT", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("MU", new int[] {2, 2, 3, 4}); - countryGroupAssignment.put("MV", new int[] {3, 2, 1, 1}); - countryGroupAssignment.put("MW", new int[] {4, 2, 1, 1}); - countryGroupAssignment.put("MX", new int[] {2, 4, 3, 2}); - countryGroupAssignment.put("MY", new int[] {2, 2, 2, 3}); - countryGroupAssignment.put("MZ", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("NA", new int[] {3, 2, 2, 1}); - countryGroupAssignment.put("NC", new int[] {2, 1, 3, 2}); - countryGroupAssignment.put("NE", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("NF", new int[] {1, 2, 2, 2}); - countryGroupAssignment.put("NG", new int[] {3, 4, 3, 2}); - countryGroupAssignment.put("NI", new int[] {3, 3, 3, 4}); - countryGroupAssignment.put("NL", new int[] {0, 2, 4, 3}); - countryGroupAssignment.put("NO", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("NP", new int[] {3, 3, 2, 2}); - countryGroupAssignment.put("NR", new int[] {4, 0, 4, 0}); - countryGroupAssignment.put("NU", new int[] {2, 2, 2, 1}); - countryGroupAssignment.put("NZ", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("OM", new int[] {2, 2, 1, 3}); - countryGroupAssignment.put("PA", new int[] {1, 3, 3, 4}); - countryGroupAssignment.put("PE", new int[] {2, 3, 4, 4}); - countryGroupAssignment.put("PF", new int[] {3, 1, 0, 1}); - countryGroupAssignment.put("PG", new int[] {4, 3, 1, 1}); - countryGroupAssignment.put("PH", new int[] {3, 0, 4, 4}); - countryGroupAssignment.put("PK", new int[] {3, 3, 3, 3}); - countryGroupAssignment.put("PL", new int[] {1, 1, 1, 3}); - countryGroupAssignment.put("PM", new int[] {0, 2, 0, 0}); - countryGroupAssignment.put("PR", new int[] {2, 1, 3, 3}); + countryGroupAssignment.put("MP", new int[] {0, 2, 1, 2}); + countryGroupAssignment.put("MQ", new int[] {2, 1, 1, 3}); + countryGroupAssignment.put("MR", new int[] {4, 2, 4, 4}); + countryGroupAssignment.put("MS", new int[] {1, 4, 3, 4}); + countryGroupAssignment.put("MT", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("MU", new int[] {2, 2, 4, 4}); + countryGroupAssignment.put("MV", new int[] {4, 3, 2, 4}); + countryGroupAssignment.put("MW", new int[] {3, 1, 1, 1}); + countryGroupAssignment.put("MX", new int[] {2, 4, 3, 3}); + countryGroupAssignment.put("MY", new int[] {2, 1, 3, 3}); + countryGroupAssignment.put("MZ", new int[] {3, 3, 3, 3}); + countryGroupAssignment.put("NA", new int[] {4, 3, 3, 3}); + countryGroupAssignment.put("NC", new int[] {2, 0, 4, 4}); + countryGroupAssignment.put("NE", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("NF", new int[] {1, 2, 2, 0}); + countryGroupAssignment.put("NG", new int[] {3, 3, 2, 2}); + countryGroupAssignment.put("NI", new int[] {3, 2, 4, 3}); + countryGroupAssignment.put("NL", new int[] {0, 2, 3, 2}); + countryGroupAssignment.put("NO", new int[] {0, 2, 1, 0}); + countryGroupAssignment.put("NP", new int[] {2, 2, 2, 2}); + countryGroupAssignment.put("NR", new int[] {4, 0, 3, 2}); + countryGroupAssignment.put("NZ", new int[] {0, 0, 1, 2}); + countryGroupAssignment.put("OM", new int[] {2, 3, 0, 2}); + countryGroupAssignment.put("PA", new int[] {1, 3, 3, 3}); + countryGroupAssignment.put("PE", new int[] {2, 4, 4, 4}); + countryGroupAssignment.put("PF", new int[] {2, 1, 1, 1}); + countryGroupAssignment.put("PG", new int[] {4, 3, 3, 2}); + countryGroupAssignment.put("PH", new int[] {3, 0, 3, 4}); + countryGroupAssignment.put("PK", new int[] {3, 2, 3, 2}); + countryGroupAssignment.put("PL", new int[] {1, 0, 1, 2}); + countryGroupAssignment.put("PM", new int[] {0, 2, 2, 0}); + countryGroupAssignment.put("PR", new int[] {2, 2, 2, 2}); countryGroupAssignment.put("PS", new int[] {3, 3, 1, 4}); - countryGroupAssignment.put("PT", new int[] {1, 1, 0, 1}); - countryGroupAssignment.put("PW", new int[] {2, 2, 1, 1}); - countryGroupAssignment.put("PY", new int[] {3, 1, 3, 3}); - countryGroupAssignment.put("QA", new int[] {2, 3, 0, 1}); + countryGroupAssignment.put("PT", new int[] {1, 1, 0, 0}); + countryGroupAssignment.put("PW", new int[] {1, 1, 3, 0}); + countryGroupAssignment.put("PY", new int[] {2, 0, 3, 3}); + countryGroupAssignment.put("QA", new int[] {2, 3, 1, 1}); countryGroupAssignment.put("RE", new int[] {1, 0, 2, 2}); countryGroupAssignment.put("RO", new int[] {0, 1, 1, 2}); countryGroupAssignment.put("RS", new int[] {1, 2, 0, 0}); - countryGroupAssignment.put("RU", new int[] {0, 1, 1, 1}); - countryGroupAssignment.put("RW", new int[] {3, 4, 2, 4}); - countryGroupAssignment.put("SA", new int[] {2, 2, 1, 2}); - countryGroupAssignment.put("SB", new int[] {4, 4, 3, 0}); + countryGroupAssignment.put("RU", new int[] {0, 1, 0, 1}); + countryGroupAssignment.put("RW", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("SA", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("SB", new int[] {4, 4, 4, 1}); countryGroupAssignment.put("SC", new int[] {4, 2, 0, 1}); - countryGroupAssignment.put("SD", new int[] {4, 4, 4, 2}); + countryGroupAssignment.put("SD", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("SE", new int[] {0, 1, 0, 0}); - countryGroupAssignment.put("SG", new int[] {1, 2, 3, 3}); - countryGroupAssignment.put("SH", new int[] {4, 4, 2, 3}); - countryGroupAssignment.put("SI", new int[] {0, 1, 0, 1}); - countryGroupAssignment.put("SJ", new int[] {0, 0, 2, 0}); - countryGroupAssignment.put("SK", new int[] {0, 1, 0, 1}); - countryGroupAssignment.put("SL", new int[] {4, 3, 2, 4}); - countryGroupAssignment.put("SM", new int[] {0, 0, 1, 3}); + countryGroupAssignment.put("SG", new int[] {1, 0, 3, 3}); + countryGroupAssignment.put("SH", new int[] {4, 2, 2, 2}); + countryGroupAssignment.put("SI", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("SJ", new int[] {2, 2, 2, 4}); + countryGroupAssignment.put("SK", new int[] {0, 1, 0, 0}); + countryGroupAssignment.put("SL", new int[] {4, 3, 3, 1}); + countryGroupAssignment.put("SM", new int[] {0, 0, 1, 2}); countryGroupAssignment.put("SN", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("SO", new int[] {4, 4, 4, 4}); - countryGroupAssignment.put("SR", new int[] {3, 2, 2, 4}); - countryGroupAssignment.put("SS", new int[] {4, 2, 4, 2}); - countryGroupAssignment.put("ST", new int[] {3, 4, 2, 2}); - countryGroupAssignment.put("SV", new int[] {2, 3, 3, 4}); + countryGroupAssignment.put("SO", new int[] {3, 4, 3, 4}); + countryGroupAssignment.put("SR", new int[] {2, 2, 2, 1}); + countryGroupAssignment.put("SS", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("ST", new int[] {2, 3, 1, 2}); + countryGroupAssignment.put("SV", new int[] {2, 2, 4, 4}); countryGroupAssignment.put("SX", new int[] {2, 4, 1, 0}); - countryGroupAssignment.put("SY", new int[] {4, 4, 1, 0}); - countryGroupAssignment.put("SZ", new int[] {3, 4, 2, 3}); - countryGroupAssignment.put("TC", new int[] {1, 1, 3, 1}); + countryGroupAssignment.put("SY", new int[] {4, 3, 1, 1}); + countryGroupAssignment.put("SZ", new int[] {4, 4, 3, 4}); + countryGroupAssignment.put("TC", new int[] {1, 2, 1, 0}); countryGroupAssignment.put("TD", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("TG", new int[] {3, 3, 1, 0}); - countryGroupAssignment.put("TH", new int[] {1, 3, 4, 4}); + countryGroupAssignment.put("TG", new int[] {3, 2, 1, 0}); + countryGroupAssignment.put("TH", new int[] {1, 3, 3, 3}); countryGroupAssignment.put("TJ", new int[] {4, 4, 4, 4}); countryGroupAssignment.put("TL", new int[] {4, 2, 4, 4}); - countryGroupAssignment.put("TM", new int[] {4, 1, 2, 3}); + countryGroupAssignment.put("TM", new int[] {4, 2, 2, 2}); countryGroupAssignment.put("TN", new int[] {2, 1, 1, 1}); - countryGroupAssignment.put("TO", new int[] {3, 3, 3, 1}); - countryGroupAssignment.put("TR", new int[] {1, 2, 0, 1}); - countryGroupAssignment.put("TT", new int[] {2, 3, 1, 2}); - countryGroupAssignment.put("TV", new int[] {4, 2, 2, 4}); - countryGroupAssignment.put("TW", new int[] {0, 0, 0, 1}); - countryGroupAssignment.put("TZ", new int[] {3, 3, 4, 3}); - countryGroupAssignment.put("UA", new int[] {0, 2, 1, 2}); - countryGroupAssignment.put("UG", new int[] {4, 3, 2, 3}); - countryGroupAssignment.put("US", new int[] {0, 1, 3, 3}); - countryGroupAssignment.put("UY", new int[] {2, 2, 2, 2}); - countryGroupAssignment.put("UZ", new int[] {3, 2, 2, 2}); - countryGroupAssignment.put("VA", new int[] {1, 2, 2, 2}); - countryGroupAssignment.put("VC", new int[] {2, 1, 0, 0}); - countryGroupAssignment.put("VE", new int[] {4, 4, 4, 3}); - countryGroupAssignment.put("VG", new int[] {2, 1, 1, 2}); - countryGroupAssignment.put("VI", new int[] {1, 0, 2, 4}); - countryGroupAssignment.put("VN", new int[] {0, 2, 4, 4}); - countryGroupAssignment.put("VU", new int[] {4, 1, 3, 1}); - countryGroupAssignment.put("WS", new int[] {3, 2, 3, 1}); + countryGroupAssignment.put("TO", new int[] {4, 3, 4, 4}); + countryGroupAssignment.put("TR", new int[] {1, 2, 1, 1}); + countryGroupAssignment.put("TT", new int[] {1, 3, 2, 4}); + countryGroupAssignment.put("TV", new int[] {4, 2, 3, 4}); + countryGroupAssignment.put("TW", new int[] {0, 0, 0, 0}); + countryGroupAssignment.put("TZ", new int[] {3, 4, 3, 3}); + countryGroupAssignment.put("UA", new int[] {0, 3, 1, 1}); + countryGroupAssignment.put("UG", new int[] {3, 2, 2, 3}); + countryGroupAssignment.put("US", new int[] {0, 1, 2, 2}); + countryGroupAssignment.put("UY", new int[] {2, 1, 2, 2}); + countryGroupAssignment.put("UZ", new int[] {2, 2, 3, 2}); + countryGroupAssignment.put("VA", new int[] {0, 2, 2, 2}); + countryGroupAssignment.put("VC", new int[] {2, 3, 0, 2}); + countryGroupAssignment.put("VE", new int[] {4, 4, 4, 4}); + countryGroupAssignment.put("VG", new int[] {3, 1, 2, 4}); + countryGroupAssignment.put("VI", new int[] {1, 4, 4, 3}); + countryGroupAssignment.put("VN", new int[] {0, 1, 3, 4}); + countryGroupAssignment.put("VU", new int[] {4, 0, 3, 3}); + countryGroupAssignment.put("WS", new int[] {3, 2, 4, 3}); countryGroupAssignment.put("XK", new int[] {1, 2, 1, 0}); - countryGroupAssignment.put("YE", new int[] {4, 4, 4, 2}); - countryGroupAssignment.put("YT", new int[] {2, 0, 2, 3}); + countryGroupAssignment.put("YE", new int[] {4, 4, 4, 3}); + countryGroupAssignment.put("YT", new int[] {2, 2, 2, 3}); countryGroupAssignment.put("ZA", new int[] {2, 3, 2, 2}); - countryGroupAssignment.put("ZM", new int[] {3, 3, 2, 1}); - countryGroupAssignment.put("ZW", new int[] {3, 3, 3, 1}); + countryGroupAssignment.put("ZM", new int[] {3, 2, 3, 3}); + countryGroupAssignment.put("ZW", new int[] {3, 3, 2, 3}); return Collections.unmodifiableMap(countryGroupAssignment); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index 69db1cfdb..98026c467 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -44,9 +44,9 @@ import java.util.Map; *

  • data: For parsing data inlined in the URI as defined in RFC 2397. *
  • udp: For fetching data over UDP (e.g. udp://something.com/media). *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), - * if constructed using {@link #DefaultDataSource(Context, TransferListener, String, - * boolean)}, or any other schemes supported by a base data source if constructed using {@link - * #DefaultDataSource(Context, TransferListener, DataSource)}. + * if constructed using {@link #DefaultDataSource(Context, String, boolean)}, or any other + * schemes supported by a base data source if constructed using {@link + * #DefaultDataSource(Context, DataSource)}. *
*/ public final class DefaultDataSource implements DataSource { @@ -72,7 +72,7 @@ public final class DefaultDataSource implements DataSource { @Nullable private DataSource dataSchemeDataSource; @Nullable private DataSource rawResourceDataSource; - private @Nullable DataSource dataSource; + @Nullable private DataSource dataSource; /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. @@ -113,7 +113,6 @@ public final class DefaultDataSource implements DataSource { context, new DefaultHttpDataSource( userAgent, - /* contentTypePredicate= */ null, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, @@ -134,85 +133,6 @@ public final class DefaultDataSource implements DataSource { transferListeners = new ArrayList<>(); } - /** - * Constructs a new instance, optionally configured to follow cross-protocol redirects. - * - * @param context A context. - * @param listener An optional listener. - * @param userAgent The User-Agent to use 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. - * @deprecated Use {@link #DefaultDataSource(Context, String, boolean)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultDataSource( - Context context, - @Nullable TransferListener listener, - String userAgent, - boolean allowCrossProtocolRedirects) { - this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects); - } - - /** - * Constructs a new instance, optionally configured to follow cross-protocol redirects. - * - * @param context A context. - * @param listener An optional listener. - * @param userAgent The User-Agent to use when requesting remote data. - * @param connectTimeoutMillis The connection timeout that should be used when requesting remote - * data, in milliseconds. A timeout of zero is interpreted as an infinite timeout. - * @param readTimeoutMillis The read timeout that should be used when requesting remote data, in - * milliseconds. A timeout of zero is interpreted as an infinite timeout. - * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP - * to HTTPS and vice versa) are enabled when fetching remote data. - * @deprecated Use {@link #DefaultDataSource(Context, String, int, int, boolean)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultDataSource( - Context context, - @Nullable TransferListener listener, - String userAgent, - int connectTimeoutMillis, - int readTimeoutMillis, - boolean allowCrossProtocolRedirects) { - this( - context, - listener, - new DefaultHttpDataSource( - userAgent, - /* contentTypePredicate= */ null, - listener, - connectTimeoutMillis, - readTimeoutMillis, - allowCrossProtocolRedirects, - /* defaultRequestProperties= */ null)); - } - - /** - * Constructs a new instance that delegates to a provided {@link DataSource} for URI schemes other - * than file, asset and content. - * - * @param context A context. - * @param listener An optional listener. - * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and - * content. This {@link DataSource} should normally support at least http(s). - * @deprecated Use {@link #DefaultDataSource(Context, DataSource)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public DefaultDataSource( - Context context, @Nullable TransferListener listener, DataSource baseDataSource) { - this(context, baseDataSource); - if (listener != null) { - transferListeners.add(listener); - } - } - @Override public void addTransferListener(TransferListener transferListener) { baseDataSource.addTransferListener(transferListener); @@ -263,7 +183,8 @@ public final class DefaultDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSource == null ? null : dataSource.getUri(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java index 9639b4ede..6b1131a3b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.upstream.DataSource.Factory; public final class DefaultDataSourceFactory implements Factory { private final Context context; - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; private final DataSource.Factory baseDataSourceFactory; /** @@ -51,7 +51,7 @@ public final class DefaultDataSourceFactory implements Factory { * @param context A context. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} * for {@link DefaultDataSource}. - * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) + * @see DefaultDataSource#DefaultDataSource(Context, DataSource) */ public DefaultDataSourceFactory(Context context, DataSource.Factory baseDataSourceFactory) { this(context, /* listener= */ null, baseDataSourceFactory); @@ -62,7 +62,7 @@ public final class DefaultDataSourceFactory implements Factory { * @param listener An optional listener. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} * for {@link DefaultDataSource}. - * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource) + * @see DefaultDataSource#DefaultDataSource(Context, DataSource) */ public DefaultDataSourceFactory( Context context, diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 87f95a32a..ec11ad234 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -16,10 +16,10 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.metadata.icy.IcyHeaders; import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -36,25 +36,29 @@ import java.net.NoRouteToHostException; import java.net.ProtocolException; import java.net.URL; import java.util.Collections; +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; +import java.util.zip.GZIPInputStream; /** * An {@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, - * RequestProperties)} constructor and passing {@code true} as the second last argument. + * #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} constructor and passing + * {@code true} for the {@code allowCrossProtocolRedirects} argument. + * + *

Note: HTTP request headers will be set using all parameters passed via (in order of decreasing + * priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to + * construct the instance. */ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSource { - /** - * The default connection timeout, in milliseconds. - */ + /** The default connection timeout, in milliseconds. */ public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; /** * The default read timeout, in milliseconds. @@ -74,13 +78,13 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private final int connectTimeoutMillis; private final int readTimeoutMillis; private final String userAgent; - private final @Nullable Predicate contentTypePredicate; - private final @Nullable RequestProperties defaultRequestProperties; + @Nullable private final RequestProperties defaultRequestProperties; private final RequestProperties requestProperties; - private @Nullable DataSpec dataSpec; - private @Nullable HttpURLConnection connection; - private @Nullable InputStream inputStream; + @Nullable private Predicate contentTypePredicate; + @Nullable private DataSpec dataSpec; + @Nullable private HttpURLConnection connection; + @Nullable private InputStream inputStream; private boolean opened; private int responseCode; @@ -92,7 +96,50 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou /** @param userAgent The User-Agent string that should be used. */ public DefaultHttpDataSource(String userAgent) { - this(userAgent, /* contentTypePredicate= */ null); + this(userAgent, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @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, int connectTimeoutMillis, int readTimeoutMillis) { + this( + userAgent, + connectTimeoutMillis, + readTimeoutMillis, + /* allowCrossProtocolRedirects= */ false, + /* defaultRequestProperties= */ null); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @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. + * @param defaultRequestProperties The default request properties to be sent to the server as HTTP + * headers or {@code null} if not required. + */ + public DefaultHttpDataSource( + String userAgent, + int connectTimeoutMillis, + int readTimeoutMillis, + boolean allowCrossProtocolRedirects, + @Nullable RequestProperties defaultRequestProperties) { + super(/* isNetwork= */ true); + this.userAgent = Assertions.checkNotEmpty(userAgent); + this.requestProperties = new RequestProperties(); + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.defaultRequestProperties = defaultRequestProperties; } /** @@ -100,7 +147,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @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)}. + * @deprecated Use {@link #DefaultHttpDataSource(String)} and {@link + * #setContentTypePredicate(Predicate)}. */ + @Deprecated public DefaultHttpDataSource(String userAgent, @Nullable Predicate contentTypePredicate) { this( userAgent, @@ -118,7 +168,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * interpreted as an infinite timeout. * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted as * an infinite timeout. + * @deprecated Use {@link #DefaultHttpDataSource(String, int, int)} and {@link + * #setContentTypePredicate(Predicate)}. */ + @SuppressWarnings("deprecation") + @Deprecated public DefaultHttpDataSource( String userAgent, @Nullable Predicate contentTypePredicate, @@ -147,7 +201,10 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * to HTTPS and vice versa) are enabled. * @param defaultRequestProperties The default request properties to be sent to the server as HTTP * headers or {@code null} if not required. + * @deprecated Use {@link #DefaultHttpDataSource(String, int, int, boolean, RequestProperties)} + * and {@link #setContentTypePredicate(Predicate)}. */ + @Deprecated public DefaultHttpDataSource( String userAgent, @Nullable Predicate contentTypePredicate, @@ -166,90 +223,19 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** - * @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. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate)} and {@link - * #addTransferListener(TransferListener)}. + * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a + * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. + * + * @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a + * predicate that was previously set. */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable 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. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate, int, int)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener, - int connectTimeoutMillis, - int readTimeoutMillis) { - this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false, - 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. - * @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. - * @param defaultRequestProperties The default request properties to be sent to the server as HTTP - * headers or {@code null} if not required. - * @deprecated Use {@link #DefaultHttpDataSource(String, Predicate, int, int, boolean, - * RequestProperties)} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - public DefaultHttpDataSource( - String userAgent, - @Nullable Predicate contentTypePredicate, - @Nullable TransferListener listener, - int connectTimeoutMillis, - int readTimeoutMillis, - boolean allowCrossProtocolRedirects, - @Nullable RequestProperties defaultRequestProperties) { - this( - userAgent, - contentTypePredicate, - connectTimeoutMillis, - readTimeoutMillis, - allowCrossProtocolRedirects, - defaultRequestProperties); - if (listener != null) { - addTransferListener(listener); - } + public void setContentTypePredicate(@Nullable Predicate contentTypePredicate) { + this.contentTypePredicate = contentTypePredicate; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return connection == null ? null : Uri.parse(connection.getURL().toString()); } @@ -281,6 +267,9 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou requestProperties.clear(); } + /** + * Opens the source to read the specified data. + */ @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { this.dataSpec = dataSpec; @@ -290,8 +279,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou try { connection = makeConnection(dataSpec); } catch (IOException e) { - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } String responseMessage; @@ -300,8 +289,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou responseMessage = connection.getResponseMessage(); } catch (IOException e) { closeConnectionQuietly(); - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } // Check for a valid response code. @@ -329,7 +318,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; // Determine the length of the data to be read, after skipping. - if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { + boolean isCompressed = isCompressed(connection); + if (!isCompressed) { if (dataSpec.length != C.LENGTH_UNSET) { bytesToRead = dataSpec.length; } else { @@ -339,14 +329,16 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } 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. + // will be that of the compressed data, which isn't what we want. Always use the dataSpec + // length in this case. bytesToRead = dataSpec.length; } try { inputStream = connection.getInputStream(); + if (isCompressed) { + inputStream = new GZIPInputStream(inputStream); + } } catch (IOException e) { closeConnectionQuietly(); throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN); @@ -440,7 +432,6 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou long position = dataSpec.position; long length = dataSpec.length; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); - boolean allowIcyMetadata = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_ICY_METADATA); if (!allowCrossProtocolRedirects) { // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection @@ -452,8 +443,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou position, length, allowGzip, - allowIcyMetadata, - /* followRedirects= */ true); + /* followRedirects= */ true, + dataSpec.httpRequestHeaders); } // We need to handle redirects ourselves to allow cross-protocol redirects. @@ -467,8 +458,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou position, length, allowGzip, - allowIcyMetadata, - /* followRedirects= */ false); + /* followRedirects= */ false, + dataSpec.httpRequestHeaders); int responseCode = connection.getResponseCode(); String location = connection.getHeaderField("Location"); if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD) @@ -508,8 +499,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou * @param position The byte offset of the requested data. * @param length The length of the requested data, or {@link C#LENGTH_UNSET}. * @param allowGzip Whether to allow the use of gzip. - * @param allowIcyMetadata Whether to allow ICY metadata. * @param followRedirects Whether to follow redirects. + * @param requestParameters parameters (HTTP headers) to include in request. */ private HttpURLConnection makeConnection( URL url, @@ -518,20 +509,24 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou long position, long length, boolean allowGzip, - boolean allowIcyMetadata, - boolean followRedirects) + boolean followRedirects, + Map requestParameters) throws IOException { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + HttpURLConnection connection = openConnection(url); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); + + Map requestHeaders = new HashMap<>(); if (defaultRequestProperties != null) { - for (Map.Entry property : defaultRequestProperties.getSnapshot().entrySet()) { - connection.setRequestProperty(property.getKey(), property.getValue()); - } + requestHeaders.putAll(defaultRequestProperties.getSnapshot()); } - for (Map.Entry property : requestProperties.getSnapshot().entrySet()) { + requestHeaders.putAll(requestProperties.getSnapshot()); + requestHeaders.putAll(requestParameters); + + for (Map.Entry property : requestHeaders.entrySet()) { connection.setRequestProperty(property.getKey(), property.getValue()); } + if (!(position == 0 && length == C.LENGTH_UNSET)) { String rangeRequest = "bytes=" + position + "-"; if (length != C.LENGTH_UNSET) { @@ -540,17 +535,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou connection.setRequestProperty("Range", rangeRequest); } connection.setRequestProperty("User-Agent", userAgent); - if (!allowGzip) { - connection.setRequestProperty("Accept-Encoding", "identity"); - } - if (allowIcyMetadata) { - connection.setRequestProperty( - IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_NAME, - IcyHeaders.REQUEST_HEADER_ENABLE_METADATA_VALUE); - } + connection.setRequestProperty("Accept-Encoding", allowGzip ? "gzip" : "identity"); connection.setInstanceFollowRedirects(followRedirects); connection.setDoOutput(httpBody != null); connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod)); + if (httpBody != null) { connection.setFixedLengthStreamingMode(httpBody.length); connection.connect(); @@ -563,6 +552,12 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou return connection; } + /** Creates an {@link HttpURLConnection} that is connected with the {@code url}. */ + @VisibleForTesting + /* package */ HttpURLConnection openConnection(URL url) throws IOException { + return (HttpURLConnection) url.openConnection(); + } + /** * Handles a redirect. * @@ -771,4 +766,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } } + private static boolean isCompressed(HttpURLConnection connection) { + String contentEncoding = connection.getHeaderField("Content-Encoding"); + return "gzip".equalsIgnoreCase(contentEncoding); + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index 371343857..f5d7dbd24 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.util.Assertions; public final class DefaultHttpDataSourceFactory extends BaseFactory { private final String userAgent; - private final @Nullable TransferListener listener; + @Nullable private final TransferListener listener; private final int connectTimeoutMillis; private final int readTimeoutMillis; private final boolean allowCrossProtocolRedirects; @@ -107,7 +107,6 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { DefaultHttpDataSource dataSource = new DefaultHttpDataSource( userAgent, - /* contentTypePredicate= */ null, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java index 307652f45..435f4bf57 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DefaultLoadErrorHandlingPolicy.java @@ -71,6 +71,7 @@ public class DefaultLoadErrorHandlingPolicy implements LoadErrorHandlingPolicy { int responseCode = ((InvalidResponseCodeException) exception).responseCode; return responseCode == 404 // HTTP 404 Not Found. || responseCode == 410 // HTTP 410 Gone. + || responseCode == 416 // HTTP 416 Range Not Satisfiable. ? DEFAULT_TRACK_BLACKLIST_MS : C.TIME_UNSET; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java index 026bc0b9c..4124a2531 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/DummyDataSource.java @@ -42,17 +42,18 @@ public final class DummyDataSource implements DataSource { } @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { + public int read(byte[] buffer, int offset, int readLength) { throw new UnsupportedOperationException(); } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return null; } @Override - public void close() throws IOException { + public void close() { // do nothing. } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java index 3cfdc4812..2661469ef 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/FileDataSource.java @@ -15,29 +15,61 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; +import android.text.TextUtils; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; /** A {@link DataSource} for reading local files. */ public final class FileDataSource extends BaseDataSource { - /** - * Thrown when IOException is encountered during local file read operation. - */ + /** Thrown when a {@link FileDataSource} encounters an error reading a file. */ public static class FileDataSourceException extends IOException { public FileDataSourceException(IOException cause) { super(cause); } + public FileDataSourceException(String message, IOException cause) { + super(message, cause); + } } - private @Nullable RandomAccessFile file; - private @Nullable Uri uri; + /** {@link DataSource.Factory} for {@link FileDataSource} instances. */ + public static final class Factory implements DataSource.Factory { + + @Nullable private TransferListener listener; + + /** + * Sets a {@link TransferListener} for {@link FileDataSource} instances created by this factory. + * + * @param listener The {@link TransferListener}. + * @return This factory. + */ + public Factory setListener(@Nullable TransferListener listener) { + this.listener = listener; + return this; + } + + @Override + public FileDataSource createDataSource() { + FileDataSource dataSource = new FileDataSource(); + if (listener != null) { + dataSource.addTransferListener(listener); + } + return dataSource; + } + } + + @Nullable private RandomAccessFile file; + @Nullable private Uri uri; private long bytesRemaining; private boolean opened; @@ -45,24 +77,16 @@ public final class FileDataSource extends BaseDataSource { super(/* isNetwork= */ false); } - /** - * @param listener An optional listener. - * @deprecated Use {@link #FileDataSource()} and {@link #addTransferListener(TransferListener)} - */ - @Deprecated - public FileDataSource(@Nullable TransferListener listener) { - this(); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws FileDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; + transferInitializing(dataSpec); - file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); + + this.file = openLocalFile(uri); + file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? file.length() - dataSpec.position : dataSpec.length; @@ -79,6 +103,23 @@ public final class FileDataSource extends BaseDataSource { return bytesRemaining; } + private static RandomAccessFile openLocalFile(Uri uri) throws FileDataSourceException { + try { + return new RandomAccessFile(Assertions.checkNotNull(uri.getPath()), "r"); + } catch (FileNotFoundException e) { + if (!TextUtils.isEmpty(uri.getQuery()) || !TextUtils.isEmpty(uri.getFragment())) { + throw new FileDataSourceException( + String.format( + "uri has query and/or fragment, which are not supported. Did you call Uri.parse()" + + " on a string containing '?' or '#'? Use Uri.fromFile(new File(path)) to" + + " avoid this. path=%s,query=%s,fragment=%s", + uri.getPath(), uri.getQuery(), uri.getFragment()), + e); + } + throw new FileDataSourceException(e); + } + } + @Override public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException { if (readLength == 0) { @@ -88,7 +129,8 @@ public final class FileDataSource extends BaseDataSource { } else { int bytesRead; try { - bytesRead = file.read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); + bytesRead = + castNonNull(file).read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); } catch (IOException e) { throw new FileDataSourceException(e); } @@ -103,7 +145,8 @@ public final class FileDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java index c077c8876..004a68fda 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java @@ -17,28 +17,22 @@ package com.google.android.exoplayer2.upstream; import androidx.annotation.Nullable; -/** - * A {@link DataSource.Factory} that produces {@link FileDataSource}. - */ +/** @deprecated Use {@link FileDataSource.Factory}. */ +@Deprecated public final class FileDataSourceFactory implements DataSource.Factory { - private final @Nullable TransferListener listener; + private final FileDataSource.Factory wrappedFactory; public FileDataSourceFactory() { - this(null); + this(/* listener= */ null); } public FileDataSourceFactory(@Nullable TransferListener listener) { - this.listener = listener; + wrappedFactory = new FileDataSource.Factory().setListener(listener); } @Override public FileDataSource createDataSource() { - FileDataSource dataSource = new FileDataSource(); - if (listener != null) { - dataSource.addTransferListener(listener); - } - return dataSource; + return wrappedFactory.createDataSource(); } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java index 97ece840a..63cad8786 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/HttpDataSource.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.upstream; +import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -183,18 +183,21 @@ public interface HttpDataSource extends DataSource { return defaultRequestProperties; } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void setDefaultRequestProperty(String name, String value) { defaultRequestProperties.set(name, value); } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void clearDefaultRequestProperty(String name) { defaultRequestProperties.remove(name); } + /** @deprecated Use {@link #getDefaultRequestProperties} instead. */ @Deprecated @Override public final void clearAllDefaultRequestProperties() { @@ -323,6 +326,13 @@ public interface HttpDataSource extends DataSource { } + /** + * Opens the source to read the specified data. + * + *

Note: {@link HttpDataSource} implementations are advised to set request headers passed via + * (in order of decreasing priority) the {@code dataSpec}, {@link #setRequestProperty} and the + * default parameters set in the {@link Factory}. + */ @Override long open(DataSpec dataSpec) throws HttpDataSourceException; @@ -336,6 +346,10 @@ public interface HttpDataSource extends DataSource { * Sets the value of a request header. The value will be used for subsequent connections * established by the source. * + *

Note: If the same header is set as a default parameter in the {@link Factory}, then the + * header value set with this method should be preferred when connecting with the data source. See + * {@link #open}. + * * @param name The name of the header field. * @param value The value of the field. */ diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index cfe46ac98..5b7846f5c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -32,6 +32,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; /** * Manages the background loading of {@link Loadable}s. @@ -56,6 +57,21 @@ public final class Loader implements LoaderErrorThrower { /** * Cancels the load. + * + *

Loadable implementations should ensure that a currently executing {@link #load()} call + * will exit reasonably quickly after this method is called. The {@link #load()} call may exit + * either by returning or by throwing an {@link IOException}. + * + *

If there is a currently executing {@link #load()} call, then the thread on which that call + * is being made will be interrupted immediately after the call to this method. Hence + * implementations do not need to (and should not attempt to) interrupt the loading thread + * themselves. + * + *

Although the loading thread will be interrupted, Loadable implementations should not use + * the interrupted status of the loading thread in {@link #load()} to determine whether the load + * has been canceled. This approach is not robust [Internal ref: b/79223737]. Instead, + * implementations should use their own flag to signal cancelation (for example, using {@link + * AtomicBoolean}). */ void cancelLoad(); @@ -158,12 +174,12 @@ public final class Loader implements LoaderErrorThrower { /** Retries the load using the default delay and resets the error count. */ public static final LoadErrorAction RETRY_RESET_ERROR_COUNT = createRetryAction(/* resetErrorCount= */ true, C.TIME_UNSET); - /** Discards the failed loading task and ignores any errors that have occurred. */ + /** Discards the failed {@link Loadable} and ignores any errors that have occurred. */ public static final LoadErrorAction DONT_RETRY = new LoadErrorAction(ACTION_TYPE_DONT_RETRY, C.TIME_UNSET); /** - * Discards the failed load. The next call to {@link #maybeThrowError()} will throw the last load - * error. + * Discards the failed {@link Loadable}. The next call to {@link #maybeThrowError()} will throw + * the last load error. */ public static final LoadErrorAction DONT_RETRY_FATAL = new LoadErrorAction(ACTION_TYPE_DONT_RETRY_FATAL, C.TIME_UNSET); @@ -190,8 +206,8 @@ public final class Loader implements LoaderErrorThrower { private final ExecutorService downloadExecutorService; - private LoadTask currentTask; - private IOException fatalError; + @Nullable private LoadTask currentTask; + @Nullable private IOException fatalError; /** * @param threadName A name for the loader's thread. @@ -242,39 +258,34 @@ public final class Loader implements LoaderErrorThrower { */ public long startLoading( T loadable, Callback callback, int defaultMinRetryCount) { - Looper looper = Looper.myLooper(); - Assertions.checkState(looper != null); + Looper looper = Assertions.checkStateNotNull(Looper.myLooper()); fatalError = null; long startTimeMs = SystemClock.elapsedRealtime(); new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0); return startTimeMs; } - /** - * Returns whether the {@link Loader} is currently loading a {@link Loadable}. - */ + /** Returns whether the loader is currently loading. */ public boolean isLoading() { return currentTask != null; } /** - * Cancels the current load. This method should only be called when a load is in progress. + * Cancels the current load. + * + * @throws IllegalStateException If the loader is not currently loading. */ public void cancelLoading() { - currentTask.cancel(false); + Assertions.checkStateNotNull(currentTask).cancel(false); } - /** - * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer - * required. - */ + /** Releases the loader. This method should be called when the loader is no longer required. */ public void release() { release(null); } /** - * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer - * required. + * Releases the loader. This method should be called when the loader is no longer required. * * @param callback An optional callback to be called on the loading thread once the loader has * been released. @@ -314,22 +325,21 @@ public final class Loader implements LoaderErrorThrower { private static final String TAG = "LoadTask"; private static final int MSG_START = 0; - private static final int MSG_CANCEL = 1; - private static final int MSG_END_OF_SOURCE = 2; - private static final int MSG_IO_EXCEPTION = 3; - private static final int MSG_FATAL_ERROR = 4; + private static final int MSG_FINISH = 1; + private static final int MSG_IO_EXCEPTION = 2; + private static final int MSG_FATAL_ERROR = 3; public final int defaultMinRetryCount; private final T loadable; private final long startTimeMs; - private @Nullable Loader.Callback callback; - private IOException currentError; + @Nullable private Loader.Callback callback; + @Nullable private IOException currentError; private int errorCount; - private volatile Thread executorThread; - private volatile boolean canceled; + @Nullable private Thread executorThread; + private boolean canceled; private volatile boolean released; public LoadTask(Looper looper, T loadable, Loader.Callback callback, @@ -361,21 +371,28 @@ public final class Loader implements LoaderErrorThrower { this.released = released; currentError = null; if (hasMessages(MSG_START)) { + // The task has not been given to the executor yet. + canceled = true; removeMessages(MSG_START); if (!released) { - sendEmptyMessage(MSG_CANCEL); + sendEmptyMessage(MSG_FINISH); } } else { - canceled = true; - loadable.cancelLoad(); - if (executorThread != null) { - executorThread.interrupt(); + // The task has been given to the executor. + synchronized (this) { + canceled = true; + loadable.cancelLoad(); + @Nullable Thread executorThread = this.executorThread; + if (executorThread != null) { + executorThread.interrupt(); + } } } if (released) { finish(); long nowMs = SystemClock.elapsedRealtime(); - callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); + Assertions.checkNotNull(callback) + .onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); // If loading, this task will be referenced from a GC root (the loading thread) until // cancellation completes. The time taken for cancellation to complete depends on the // implementation of the Loadable that the task is loading. We null the callback reference @@ -387,8 +404,12 @@ public final class Loader implements LoaderErrorThrower { @Override public void run() { try { - executorThread = Thread.currentThread(); - if (!canceled) { + boolean shouldLoad; + synchronized (this) { + shouldLoad = !canceled; + executorThread = Thread.currentThread(); + } + if (shouldLoad) { TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName()); try { loadable.load(); @@ -396,8 +417,13 @@ public final class Loader implements LoaderErrorThrower { TraceUtil.endSection(); } } + synchronized (this) { + executorThread = null; + // Clear the interrupted flag if set, to avoid it leaking into a subsequent task. + Thread.interrupted(); + } if (!released) { - sendEmptyMessage(MSG_END_OF_SOURCE); + sendEmptyMessage(MSG_FINISH); } } catch (IOException e) { if (!released) { @@ -407,7 +433,7 @@ public final class Loader implements LoaderErrorThrower { // The load was canceled. Assertions.checkState(canceled); if (!released) { - sendEmptyMessage(MSG_END_OF_SOURCE); + sendEmptyMessage(MSG_FINISH); } } catch (Exception e) { // This should never happen, but handle it anyway. @@ -450,15 +476,13 @@ public final class Loader implements LoaderErrorThrower { finish(); long nowMs = SystemClock.elapsedRealtime(); long durationMs = nowMs - startTimeMs; + Loader.Callback callback = Assertions.checkNotNull(this.callback); if (canceled) { callback.onLoadCanceled(loadable, nowMs, durationMs, false); return; } switch (msg.what) { - case MSG_CANCEL: - callback.onLoadCanceled(loadable, nowMs, durationMs, false); - break; - case MSG_END_OF_SOURCE: + case MSG_FINISH: try { callback.onLoadCompleted(loadable, nowMs, durationMs); } catch (RuntimeException e) { @@ -492,7 +516,7 @@ public final class Loader implements LoaderErrorThrower { private void execute() { currentError = null; - downloadExecutorService.execute(currentTask); + downloadExecutorService.execute(Assertions.checkNotNull(currentTask)); } private void finish() { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java index 62e68cd92..767b6d78a 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/PriorityDataSource.java @@ -71,7 +71,8 @@ public final class PriorityDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 1f0313594..fbfd69861 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -15,13 +15,16 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.net.Uri; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.FileInputStream; import java.io.IOException; @@ -64,9 +67,9 @@ public final class RawResourceDataSource extends BaseDataSource { private final Resources resources; - private @Nullable Uri uri; - private @Nullable AssetFileDescriptor assetFileDescriptor; - private @Nullable InputStream inputStream; + @Nullable private Uri uri; + @Nullable private AssetFileDescriptor assetFileDescriptor; + @Nullable private InputStream inputStream; private long bytesRemaining; private boolean opened; @@ -78,38 +81,31 @@ public final class RawResourceDataSource extends BaseDataSource { this.resources = context.getResources(); } - /** - * @param context A context. - * @param listener An optional listener. - * @deprecated Use {@link #RawResourceDataSource(Context)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public RawResourceDataSource(Context context, @Nullable TransferListener listener) { - this(context); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws RawResourceDataSourceException { try { - uri = dataSpec.uri; + Uri uri = dataSpec.uri; + this.uri = uri; if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) { throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME); } int resourceId; try { - resourceId = Integer.parseInt(uri.getLastPathSegment()); + resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment())); } catch (NumberFormatException e) { throw new RawResourceDataSourceException("Resource identifier must be an integer."); } transferInitializing(dataSpec); - assetFileDescriptor = resources.openRawResourceFd(resourceId); - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); + this.assetFileDescriptor = assetFileDescriptor; + if (assetFileDescriptor == null) { + throw new RawResourceDataSourceException("Resource is compressed: " + uri); + } + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + inputStream.skip(assetFileDescriptor.getStartOffset()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { @@ -147,7 +143,7 @@ public final class RawResourceDataSource extends BaseDataSource { try { int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? readLength : (int) Math.min(bytesRemaining, readLength); - bytesRead = inputStream.read(buffer, offset, bytesToRead); + bytesRead = castNonNull(inputStream).read(buffer, offset, bytesToRead); } catch (IOException e) { throw new RawResourceDataSourceException(e); } @@ -167,7 +163,8 @@ public final class RawResourceDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java index b7a01505f..6cdc381ba 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/StatsDataSource.java @@ -96,7 +96,8 @@ public final class StatsDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return dataSource.getUri(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java index ecf25f2eb..f56f19a6c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/TeeDataSource.java @@ -80,7 +80,8 @@ public final class TeeDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java index e7aab31cc..4d9b37533 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/UdpDataSource.java @@ -52,11 +52,11 @@ public final class UdpDataSource extends BaseDataSource { private final byte[] packetBuffer; private final DatagramPacket packet; - private @Nullable Uri uri; - private @Nullable DatagramSocket socket; - private @Nullable MulticastSocket multicastSocket; - private @Nullable InetAddress address; - private @Nullable InetSocketAddress socketAddress; + @Nullable private Uri uri; + @Nullable private DatagramSocket socket; + @Nullable private MulticastSocket multicastSocket; + @Nullable private InetAddress address; + @Nullable private InetSocketAddress socketAddress; private boolean opened; private int packetRemaining; @@ -88,50 +88,6 @@ public final class UdpDataSource extends BaseDataSource { packet = new DatagramPacket(packetBuffer, 0, maxPacketSize); } - /** - * Constructs a new instance. - * - * @param listener An optional listener. - * @deprecated Use {@link #UdpDataSource()} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public UdpDataSource(@Nullable TransferListener listener) { - this(listener, DEFAULT_MAX_PACKET_SIZE); - } - - /** - * Constructs a new instance. - * - * @param listener An optional listener. - * @param maxPacketSize The maximum datagram packet size, in bytes. - * @deprecated Use {@link #UdpDataSource(int)} and {@link #addTransferListener(TransferListener)}. - */ - @Deprecated - @SuppressWarnings("deprecation") - public UdpDataSource(@Nullable TransferListener listener, int maxPacketSize) { - this(listener, maxPacketSize, DEFAULT_SOCKET_TIMEOUT_MILLIS); - } - - /** - * Constructs a new instance. - * - * @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. - * @deprecated Use {@link #UdpDataSource(int, int)} and {@link - * #addTransferListener(TransferListener)}. - */ - @Deprecated - public UdpDataSource( - @Nullable TransferListener listener, int maxPacketSize, int socketTimeoutMillis) { - this(maxPacketSize, socketTimeoutMillis); - if (listener != null) { - addTransferListener(listener); - } - } - @Override public long open(DataSpec dataSpec) throws UdpDataSourceException { uri = dataSpec.uri; @@ -188,7 +144,8 @@ public final class UdpDataSource extends BaseDataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return uri; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java index 12905f908..1d504159e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/Cache.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; import java.io.File; import java.io.IOException; @@ -100,7 +101,10 @@ public interface Cache { /** * Releases the cache. This method must be called when the cache is no longer required. The cache * must not be used after calling this method. + * + *

This method may be slow and shouldn't normally be called on the main thread. */ + @WorkerThread void release(); /** @@ -162,23 +166,29 @@ public interface Cache { * calling {@link #commitFile(File, long)}. When the caller has finished writing, it must release * the lock by calling {@link #releaseHoleSpan}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. * @throws InterruptedException If the thread was interrupted. * @throws CacheException If an error is encountered. */ + @WorkerThread CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException; /** * 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}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @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. * @throws CacheException If an error is encountered. */ + @WorkerThread @Nullable CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; @@ -186,6 +196,8 @@ public interface Cache { * 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)}. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param key The cache key for the data. * @param position The starting position of the data. * @param length The length of the data being written, or {@link C#LENGTH_UNSET} if unknown. Used @@ -193,16 +205,20 @@ public interface Cache { * @return The file into which data should be written. * @throws CacheException If an error is encountered. */ + @WorkerThread File startFile(String key, long position, long length) throws CacheException; /** * Commits a file into the cache. Must only be called when holding a corresponding hole {@link - * CacheSpan} obtained from {@link #startReadWrite(String, long)} + * CacheSpan} obtained from {@link #startReadWrite(String, long)}. + * + *

This method may be slow and shouldn't normally be called on the main thread. * * @param file A newly written cache file. * @param length The length of the newly written cache file in bytes. * @throws CacheException If an error is encountered. */ + @WorkerThread void commitFile(File file, long length) throws CacheException; /** @@ -216,19 +232,22 @@ public interface Cache { /** * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param span The {@link CacheSpan} to remove. * @throws CacheException If an error is encountered. */ + @WorkerThread void removeSpan(CacheSpan span) throws CacheException; - /** - * 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; - */ + /** + * 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); /** @@ -247,10 +266,13 @@ public interface Cache { * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link * CachedContent} is added if there isn't one already with the given key. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param key The cache key for the data. * @param mutations Contains mutations to be applied to the metadata. * @throws CacheException If an error is encountered. */ + @WorkerThread void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) throws CacheException; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 319128b62..22ed3892e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -185,7 +185,6 @@ public final class CacheDataSink implements DataSink { outputStreamBytesWritten = 0; } - @SuppressWarnings("ThrowFromFinallyBlock") private void closeCurrentOutputStream() throws IOException { if (outputStream == null) { return; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 12107e611..94ec2c6df 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -123,7 +123,7 @@ public final class CacheDataSource implements DataSource { private final Cache cache; private final DataSource cacheReadDataSource; - private final @Nullable DataSource cacheWriteDataSource; + @Nullable private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; private final CacheKeyFactory cacheKeyFactory; @Nullable private final EventListener eventListener; @@ -132,16 +132,18 @@ public final class CacheDataSource implements DataSource { private final boolean ignoreCacheOnError; private final boolean ignoreCacheForUnsetLengthRequests; - private @Nullable DataSource currentDataSource; + @Nullable private DataSource currentDataSource; private boolean currentDataSpecLengthUnset; @Nullable private Uri uri; @Nullable private Uri actualUri; @HttpMethod private int httpMethod; - private int flags; - private @Nullable String key; + @Nullable private byte[] httpBody; + private Map httpRequestHeaders = Collections.emptyMap(); + @DataSpec.Flags private int flags; + @Nullable private String key; private long readPosition; private long bytesRemaining; - private @Nullable CacheSpan currentHoleSpan; + @Nullable private CacheSpan currentHoleSpan; private boolean seenCacheError; private boolean currentRequestIgnoresCache; private long totalCachedBytesRead; @@ -261,6 +263,8 @@ public final class CacheDataSource implements DataSource { uri = dataSpec.uri; actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); httpMethod = dataSpec.httpMethod; + httpBody = dataSpec.httpBody; + httpRequestHeaders = dataSpec.httpRequestHeaders; flags = dataSpec.flags; readPosition = dataSpec.position; @@ -332,7 +336,8 @@ public final class CacheDataSource implements DataSource { } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return actualUri; } @@ -349,6 +354,11 @@ public final class CacheDataSource implements DataSource { uri = null; actualUri = null; httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; + httpRequestHeaders = Collections.emptyMap(); + flags = 0; + readPosition = 0; + key = null; notifyBytesRead(); try { closeCurrentSource(); @@ -395,7 +405,15 @@ public final class CacheDataSource implements DataSource { nextDataSource = upstreamDataSource; nextDataSpec = new DataSpec( - uri, httpMethod, null, readPosition, readPosition, bytesRemaining, key, flags); + uri, + httpMethod, + httpBody, + readPosition, + readPosition, + bytesRemaining, + key, + flags, + httpRequestHeaders); } else if (nextSpan.isCached) { // Data is cached, read from cache. Uri fileUri = Uri.fromFile(nextSpan.file); @@ -404,6 +422,8 @@ public final class CacheDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { length = Math.min(length, bytesRemaining); } + // Deliberately skip the HTTP-related parameters since we're reading from the cache, not + // making an HTTP request. nextDataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); nextDataSource = cacheReadDataSource; } else { @@ -418,7 +438,16 @@ public final class CacheDataSource implements DataSource { } } nextDataSpec = - new DataSpec(uri, httpMethod, null, readPosition, readPosition, length, key, flags); + new DataSpec( + uri, + httpMethod, + httpBody, + readPosition, + readPosition, + length, + key, + flags, + httpRequestHeaders); if (cacheWriteDataSource != null) { nextDataSource = cacheWriteDataSource; } else { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java index 2f0f6caa2..21758bdce 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.FileDataSourceFactory; +import com.google.android.exoplayer2.upstream.FileDataSource; /** A {@link DataSource.Factory} that produces {@link CacheDataSource}. */ public final class CacheDataSourceFactory implements DataSource.Factory { @@ -49,7 +49,7 @@ public final class CacheDataSourceFactory implements DataSource.Factory { this( cache, upstreamFactory, - new FileDataSourceFactory(), + new FileDataSource.Factory(), new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE), flags, /* eventListener= */ null); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java index 2a8b393ed..e288a5258 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -19,6 +19,7 @@ import android.content.ContentValues; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; @@ -42,7 +43,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private static final int COLUMN_INDEX_LENGTH = 1; private static final int COLUMN_INDEX_LAST_TOUCH_TIMESTAMP = 2; - private static final String WHERE_NAME_EQUALS = COLUMN_INDEX_NAME + " = ?"; + private static final String WHERE_NAME_EQUALS = COLUMN_NAME + " = ?"; private static final String[] COLUMNS = new String[] { @@ -59,22 +60,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final DatabaseProvider databaseProvider; - @MonotonicNonNull private String tableName; + private @MonotonicNonNull String tableName; /** * Deletes index data for the specified cache. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param databaseProvider Provides the database in which the index is stored. * @param uid The cache UID. * @throws DatabaseIOException If an error occurs deleting the index data. */ + @WorkerThread public static void delete(DatabaseProvider databaseProvider, long uid) throws DatabaseIOException { String hexUid = Long.toHexString(uid); try { String tableName = getTableName(hexUid); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { VersionTable.removeVersion( writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid); @@ -96,9 +100,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Initializes the index for the given cache UID. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param uid The cache UID. * @throws DatabaseIOException If an error occurs initializing the index. */ + @WorkerThread public void initialize(long uid) throws DatabaseIOException { try { String hexUid = Long.toHexString(uid); @@ -109,7 +116,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid); if (version != TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { VersionTable.setVersion( writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, hexUid, TABLE_VERSION); @@ -129,9 +136,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Returns all file metadata keyed by file name. The returned map is mutable and may be modified * by the caller. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @return The file metadata keyed by file name. * @throws DatabaseIOException If an error occurs loading the metadata. */ + @WorkerThread public Map getAll() throws DatabaseIOException { try (Cursor cursor = getCursor()) { Map fileMetadata = new HashMap<>(cursor.getCount()); @@ -150,11 +160,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Sets metadata for a given file. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param name The name of the file. * @param length The file length. * @param lastTouchTimestamp The file last touch timestamp. * @throws DatabaseIOException If an error occurs setting the metadata. */ + @WorkerThread public void set(String name, long length, long lastTouchTimestamp) throws DatabaseIOException { Assertions.checkNotNull(tableName); try { @@ -172,9 +185,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Removes metadata. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param name The name of the file whose metadata is to be removed. * @throws DatabaseIOException If an error occurs removing the metadata. */ + @WorkerThread public void remove(String name) throws DatabaseIOException { Assertions.checkNotNull(tableName); try { @@ -188,14 +204,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Removes metadata. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param names The names of the files whose metadata is to be removed. * @throws DatabaseIOException If an error occurs removing the metadata. */ + @WorkerThread public void removeAll(Set names) throws DatabaseIOException { Assertions.checkNotNull(tableName); try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { for (String name : names) { writableDatabase.delete(tableName, WHERE_NAME_EQUALS, new String[] {name}); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java index 1e8cf1517..609e933c9 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheSpan.java @@ -41,10 +41,8 @@ public class CacheSpan implements Comparable { * 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 @Nullable File file; + /** The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */ + @Nullable public final File file; /** The last touch timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */ public final long lastTouchTimestamp; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 6277ec686..9f1fc5446 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -16,8 +16,9 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceException; @@ -104,19 +105,20 @@ public final class CacheUtil { * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early * if the end of the input is reached. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. - * @param cacheKeyFactory An optional factory for cache keys. * @param upstream A {@link DataSource} for reading data not in the cache. * @param progressListener A listener to receive progress updates, or {@code null}. * @param isCanceled An optional flag that will interrupt caching if set to true. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. */ + @WorkerThread public static void cache( DataSpec dataSpec, Cache cache, - @Nullable CacheKeyFactory cacheKeyFactory, DataSource upstream, @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled) @@ -124,7 +126,7 @@ public final class CacheUtil { cache( dataSpec, cache, - cacheKeyFactory, + /* cacheKeyFactory= */ null, new CacheDataSource(cache, upstream), new byte[DEFAULT_BUFFER_SIZE_BYTES], /* priorityTaskManager= */ null, @@ -135,14 +137,16 @@ public final class CacheUtil { } /** - * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops - * early if end of input is reached and {@code enableEOFException} is false. + * Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early + * if end of input is reached and {@code enableEOFException} is false. * - *

If a {@link PriorityTaskManager} is given, it's used to pause and resume caching depending - * on {@code priority} and the priority of other tasks registered to the PriorityTaskManager. - * Please note that it's the responsibility of the calling code to call {@link - * PriorityTaskManager#add} to register with the manager before calling this method, and to call - * {@link PriorityTaskManager#remove} afterwards to unregister. + *

If a {@link PriorityTaskManager} is provided, it's used to pause and resume caching + * depending on {@code priority} and the priority of other tasks registered to the + * PriorityTaskManager. Please note that it's the responsibility of the calling code to call + * {@link PriorityTaskManager#add} to register with the manager before calling this method, and to + * call {@link PriorityTaskManager#remove} afterwards to unregister. + * + *

This method may be slow and shouldn't normally be called on the main thread. * * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. @@ -159,13 +163,14 @@ public final class CacheUtil { * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}. */ + @WorkerThread public static void cache( DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory, CacheDataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, + @Nullable PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressListener progressListener, @Nullable AtomicBoolean isCanceled, @@ -261,11 +266,11 @@ public final class CacheUtil { long length, DataSource dataSource, byte[] buffer, - PriorityTaskManager priorityTaskManager, + @Nullable PriorityTaskManager priorityTaskManager, int priority, @Nullable ProgressNotifier progressNotifier, boolean isLastBlock, - AtomicBoolean isCanceled) + @Nullable AtomicBoolean isCanceled) throws IOException, InterruptedException { long positionOffset = absoluteStreamPosition - dataSpec.absoluteStreamPosition; long initialPositionOffset = positionOffset; @@ -333,10 +338,13 @@ public final class CacheUtil { /** * Removes all of the data specified by the {@code dataSpec}. * + *

This methods blocks until the operation is complete. + * * @param dataSpec Defines the data to be removed. * @param cache A {@link Cache} to store the data. * @param cacheKeyFactory An optional factory for cache keys. */ + @WorkerThread public static void remove( DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) { remove(cache, buildCacheKey(dataSpec, cacheKeyFactory)); @@ -345,9 +353,12 @@ public final class CacheUtil { /** * Removes all of the data specified by the {@code key}. * + *

This methods blocks until the operation is complete. + * * @param cache A {@link Cache} to store the data. * @param key The key whose data should be removed. */ + @WorkerThread public static void remove(Cache cache, String key) { NavigableSet cachedSpans = cache.getCachedSpans(key); for (CacheSpan cachedSpan : cachedSpans) { @@ -379,7 +390,7 @@ public final class CacheUtil { .buildCacheKey(dataSpec); } - private static void throwExceptionIfInterruptedOrCancelled(AtomicBoolean isCanceled) + private static void throwExceptionIfInterruptedOrCancelled(@Nullable AtomicBoolean isCanceled) throws InterruptedException { if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) { throw new InterruptedException(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 7abb9b389..b6a55c8da 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkState; + import androidx.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import java.io.File; import java.util.TreeSet; @@ -115,12 +117,18 @@ import java.util.TreeSet; * @return the length of the cached or not cached data block length. */ public long getCachedBytesLength(long position, long length) { + checkArgument(position >= 0); + checkArgument(length >= 0); SimpleCacheSpan span = getSpan(position); if (span.isHoleSpan()) { // We don't have a span covering the start of the queried region. return -Math.min(span.isOpenEnded() ? Long.MAX_VALUE : span.length, length); } long queryEndPosition = position + length; + if (queryEndPosition < 0) { + // The calculation rolled over (length is probably Long.MAX_VALUE). + queryEndPosition = Long.MAX_VALUE; + } long currentEndPosition = span.position + span.length; if (currentEndPosition < queryEndPosition) { for (SimpleCacheSpan next : cachedSpans.tailSet(span, false)) { @@ -151,7 +159,7 @@ import java.util.TreeSet; */ public SimpleCacheSpan setLastTouchTimestamp( SimpleCacheSpan cacheSpan, long lastTouchTimestamp, boolean updateFile) { - Assertions.checkState(cachedSpans.remove(cacheSpan)); + checkState(cachedSpans.remove(cacheSpan)); File file = cacheSpan.file; if (updateFile) { File directory = file.getParentFile(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index bc5443f36..7e09025dd 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -21,10 +21,11 @@ import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import android.util.SparseArray; import android.util.SparseBooleanArray; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; @@ -96,7 +97,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable private Storage previousStorage; /** Returns whether the file is an index file. */ - public static final boolean isIndexFile(String fileName) { + public static boolean isIndexFile(String fileName) { // Atomic file backups add additional suffixes to the file name. return fileName.startsWith(FILE_NAME_ATOMIC); } @@ -104,10 +105,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Deletes index data for the specified cache. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param databaseProvider Provides the database in which the index is stored. * @param uid The cache UID. * @throws DatabaseIOException If an error occurs deleting the index data. */ + @WorkerThread public static void delete(DatabaseProvider databaseProvider, long uid) throws DatabaseIOException { DatabaseStorage.delete(databaseProvider, uid); @@ -174,9 +178,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Loads the index data for the given cache UID. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param uid The UID of the cache whose index is to be loaded. * @throws IOException If an error occurs initializing the index data. */ + @WorkerThread public void initialize(long uid) throws IOException { storage.initialize(uid); if (previousStorage != null) { @@ -199,8 +206,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Stores the index data to index file if there is a change. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @throws IOException If an error occurs storing the index data. */ + @WorkerThread public void store() throws IOException { storage.storeIncremental(keyToContent); // Make ids that were removed since the index was last stored eligible for re-use. @@ -787,7 +797,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; hexUid); if (version != TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { initializeTable(writableDatabase); writableDatabase.setTransactionSuccessful(); @@ -822,7 +832,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void storeFully(HashMap content) throws IOException { try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { initializeTable(writableDatabase); for (CachedContent cachedContent : content.values()) { @@ -845,7 +855,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } try { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { for (int i = 0; i < pendingUpdates.size(); i++) { CachedContent cachedContent = pendingUpdates.valueAt(i); @@ -921,7 +931,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; try { String tableName = getTableName(hexUid); SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); + writableDatabase.beginTransactionNonExclusive(); try { VersionTable.removeVersion( writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java index 44a735f14..c88e2643d 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java @@ -17,13 +17,10 @@ package com.google.android.exoplayer2.upstream.cache; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; -import java.util.Comparator; import java.util.TreeSet; -/** - * Evicts least recently used cache files first. - */ -public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator { +/** Evicts least recently used cache files first. */ +public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor { private final long maxBytes; private final TreeSet leastRecentlyUsed; @@ -32,7 +29,7 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar public LeastRecentlyUsedCacheEvictor(long maxBytes) { this.maxBytes = maxBytes; - this.leastRecentlyUsed = new TreeSet<>(this); + this.leastRecentlyUsed = new TreeSet<>(LeastRecentlyUsedCacheEvictor::compare); } @Override @@ -71,16 +68,6 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar onSpanAdded(cache, newSpan); } - @Override - public int compare(CacheSpan lhs, CacheSpan rhs) { - long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; - if (lastTouchTimestampDelta == 0) { - // Use the standard compareTo method as a tie-break. - return lhs.compareTo(rhs); - } - return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; - } - private void evictCache(Cache cache, long requiredSpace) { while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) { try { @@ -91,4 +78,12 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar } } + private static int compare(CacheSpan lhs, CacheSpan rhs) { + long lastTouchTimestampDelta = lhs.lastTouchTimestamp - rhs.lastTouchTimestamp; + if (lastTouchTimestampDelta == 0) { + // Use the standard compareTo method as a tie-break. + return lhs.compareTo(rhs); + } + return lhs.lastTouchTimestamp < rhs.lastTouchTimestamp ? -1 : 1; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index b31d3b66f..a4fade25e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.os.ConditionVariable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.database.DatabaseIOException; import com.google.android.exoplayer2.database.DatabaseProvider; @@ -61,9 +62,6 @@ public final class SimpleCache implements Cache { private static final HashSet lockedCacheDirs = new HashSet<>(); - private static boolean cacheFolderLockingDisabled; - private static boolean cacheInitializationExceptionsDisabled; - private final File cacheDir; private final CacheEvictor evictor; private final CachedContentIndex contentIndex; @@ -75,7 +73,7 @@ public final class SimpleCache implements Cache { private long uid; private long totalSpace; private boolean released; - @MonotonicNonNull private CacheException initializationException; + private @MonotonicNonNull CacheException initializationException; /** * Returns whether {@code cacheFolder} is locked by a {@link SimpleCache} instance. To unlock the @@ -85,40 +83,16 @@ public final class SimpleCache implements Cache { return lockedCacheDirs.contains(cacheFolder.getAbsoluteFile()); } - /** - * Disables locking the cache folders which {@link SimpleCache} instances are using and releases - * any previous lock. - * - *

The locking prevents multiple {@link SimpleCache} instances from being created for the same - * folder. Disabling it may cause the cache data to be corrupted. Use at your own risk. - * - * @deprecated Don't create multiple {@link SimpleCache} instances for the same cache folder. If - * you need to create another instance, make sure you call {@link #release()} on the previous - * instance. - */ - @Deprecated - public static synchronized void disableCacheFolderLocking() { - cacheFolderLockingDisabled = true; - lockedCacheDirs.clear(); - } - - /** - * Disables throwing of cache initialization exceptions. - * - * @deprecated Don't use this. Provided for problematic upgrade cases only. - */ - @Deprecated - public static void disableCacheInitializationExceptions() { - cacheInitializationExceptionsDisabled = true; - } - /** * Deletes all content belonging to a cache instance. * + *

This method may be slow and shouldn't normally be called on the main thread. + * * @param cacheDir The cache directory. * @param databaseProvider The database in which index data is stored, or {@code null} if the * cache used a legacy index. */ + @WorkerThread public static void delete(File cacheDir, @Nullable DatabaseProvider databaseProvider) { if (!cacheDir.exists()) { return; @@ -177,6 +151,7 @@ public final class SimpleCache implements Cache { * @deprecated Use a constructor that takes a {@link DatabaseProvider} for improved performance. */ @Deprecated + @SuppressWarnings("deprecation") public SimpleCache(File cacheDir, CacheEvictor evictor, @Nullable byte[] secretKey) { this(cacheDir, evictor, secretKey, secretKey != null); } @@ -304,7 +279,7 @@ public final class SimpleCache implements Cache { * @throws CacheException If an error occurred during initialization. */ public synchronized void checkInitialization() throws CacheException { - if (!cacheInitializationExceptionsDisabled && initializationException != null) { + if (initializationException != null) { throw initializationException; } } @@ -380,20 +355,21 @@ public final class SimpleCache implements Cache { } @Override - public synchronized SimpleCacheSpan startReadWrite(String key, long position) + public synchronized CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException { Assertions.checkState(!released); checkInitialization(); while (true) { - SimpleCacheSpan span = startReadWriteNonBlocking(key, position); + CacheSpan span = startReadWriteNonBlocking(key, position); 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). + // Lock not available. We'll be woken up when a span is added, or when a locked span is + // released. We'll be able to make progress when either: + // 1. A span is added for the requested key that covers the requested position, in which + // case a read can be started. + // 2. The lock for the requested key is released, in which case a write can be started. wait(); } } @@ -401,47 +377,26 @@ public final class SimpleCache implements Cache { @Override @Nullable - public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position) + public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException { Assertions.checkState(!released); checkInitialization(); SimpleCacheSpan span = getSpan(key, position); - // Read case. if (span.isCached) { - if (!touchCacheSpans) { - return span; - } - String fileName = Assertions.checkNotNull(span.file).getName(); - long length = span.length; - long lastTouchTimestamp = System.currentTimeMillis(); - boolean updateFile = false; - if (fileIndex != null) { - try { - fileIndex.set(fileName, length, lastTouchTimestamp); - } catch (IOException e) { - Log.w(TAG, "Failed to update index with new touch timestamp."); - } - } else { - // Updating the file itself to incorporate the new last touch timestamp is much slower than - // updating the file index. Hence we only update the file if we don't have a file index. - updateFile = true; - } - SimpleCacheSpan newSpan = - contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); - notifySpanTouched(span, newSpan); - return newSpan; + // Read case. + return touchSpan(key, span); } CachedContent cachedContent = contentIndex.getOrAdd(key); if (!cachedContent.isLocked()) { - // Write case, lock available. + // Write case. cachedContent.setLocked(true); return span; } - // Write case, lock not available. + // Lock not available. return null; } @@ -558,36 +513,6 @@ public final class SimpleCache implements Cache { return contentIndex.getContentMetadata(key); } - /** - * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link - * SimpleCacheSpan}. - * - *

If the lookup position is contained by an existing entry in the cache, then the returned - * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is - * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the - * maximum extents of the hole in the cache. - * - * @param key The key of the span being requested. - * @param position The position of the span being requested. - * @return The corresponding cache {@link SimpleCacheSpan}. - */ - private SimpleCacheSpan getSpan(String key, long position) { - CachedContent cachedContent = contentIndex.get(key); - if (cachedContent == null) { - return SimpleCacheSpan.createOpenHole(key, position); - } - while (true) { - SimpleCacheSpan span = cachedContent.getSpan(position); - if (span.isCached && !span.file.exists()) { - // 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(); - continue; - } - return span; - } - } - /** Ensures that the cache's in-memory representation has been initialized. */ private void initialize() { if (!cacheDir.exists()) { @@ -696,6 +621,67 @@ public final class SimpleCache implements Cache { } } + /** + * Touches a cache span, returning the updated result. If the evictor does not require cache spans + * to be touched, then this method does nothing and the span is returned without modification. + * + * @param key The key of the span being touched. + * @param span The span being touched. + * @return The updated span. + */ + private SimpleCacheSpan touchSpan(String key, SimpleCacheSpan span) { + if (!touchCacheSpans) { + return span; + } + String fileName = Assertions.checkNotNull(span.file).getName(); + long length = span.length; + long lastTouchTimestamp = System.currentTimeMillis(); + boolean updateFile = false; + if (fileIndex != null) { + try { + fileIndex.set(fileName, length, lastTouchTimestamp); + } catch (IOException e) { + Log.w(TAG, "Failed to update index with new touch timestamp."); + } + } else { + // Updating the file itself to incorporate the new last touch timestamp is much slower than + // updating the file index. Hence we only update the file if we don't have a file index. + updateFile = true; + } + SimpleCacheSpan newSpan = + contentIndex.get(key).setLastTouchTimestamp(span, lastTouchTimestamp, updateFile); + notifySpanTouched(span, newSpan); + return newSpan; + } + + /** + * Returns the cache span corresponding to the provided lookup span. + * + *

If the lookup position is contained by an existing entry in the cache, then the returned + * span defines the file in which the data is stored. If the lookup position is not contained by + * an existing entry, then the returned span defines the maximum extents of the hole in the cache. + * + * @param key The key of the span being requested. + * @param position The position of the span being requested. + * @return The corresponding cache {@link SimpleCacheSpan}. + */ + private SimpleCacheSpan getSpan(String key, long position) { + CachedContent cachedContent = contentIndex.get(key); + if (cachedContent == null) { + return SimpleCacheSpan.createOpenHole(key, position); + } + while (true) { + SimpleCacheSpan span = cachedContent.getSpan(position); + if (span.isCached && span.file.length() != span.length) { + // The file has been modified or deleted underneath us. It's likely that other files will + // have been modified too, so scan the whole in-memory representation. + removeStaleSpans(); + continue; + } + return span; + } + } + /** * Adds a cached span to the in-memory representation. * @@ -728,14 +714,14 @@ public final class SimpleCache implements Cache { } /** - * Scans all of the cached spans in the in-memory representation, removing any for which files no - * longer exist. + * Scans all of the cached spans in the in-memory representation, removing any for which the + * underlying file lengths no longer match. */ private void removeStaleSpans() { ArrayList spansToBeRemoved = new ArrayList<>(); for (CachedContent cachedContent : contentIndex.getAll()) { for (CacheSpan span : cachedContent.getSpans()) { - if (!span.file.exists()) { + if (span.file.length() != span.length) { spansToBeRemoved.add(span); } } @@ -780,7 +766,6 @@ public final class SimpleCache implements Cache { * * @param files The files belonging to the root directory. * @return The loaded UID, or {@link #UID_UNSET} if a UID has not yet been created. - * @throws IOException If there is an error loading or generating the UID. */ private static long loadUid(File[] files) { for (File file : files) { @@ -818,15 +803,10 @@ public final class SimpleCache implements Cache { } private static synchronized boolean lockFolder(File cacheDir) { - if (cacheFolderLockingDisabled) { - return true; - } return lockedCacheDirs.add(cacheDir.getAbsoluteFile()); } private static synchronized void unlockFolder(File cacheDir) { - if (!cacheFolderLockingDisabled) { - lockedCacheDirs.remove(cacheDir.getAbsoluteFile()); - } + lockedCacheDirs.remove(cacheDir.getAbsoluteFile()); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index 7d9f0c9ff..5f6ea338e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -116,10 +116,11 @@ import java.util.regex.Pattern; File file, long length, long lastTouchTimestamp, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { - file = upgradeFile(file, index); - if (file == null) { + @Nullable File upgradedFile = upgradeFile(file, index); + if (upgradedFile == null) { return null; } + file = upgradedFile; name = file.getName(); } @@ -174,8 +175,12 @@ import java.util.regex.Pattern; key = matcher.group(1); // Keys were not escaped in version 1. } - File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key), - Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); + File newCacheFile = + getCacheFile( + Assertions.checkStateNotNull(file.getParentFile()), + index.assignIdForKey(key), + Long.parseLong(matcher.group(2)), + Long.parseLong(matcher.group(3))); if (!file.renameTo(newCacheFile)) { return null; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java index 2f241404d..d9b3ff006 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import static com.google.android.exoplayer2.util.Util.castNonNull; + +import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSpec; import java.io.IOException; @@ -27,9 +30,9 @@ public final class AesCipherDataSink implements DataSink { private final DataSink wrappedDataSink; private final byte[] secretKey; - private final byte[] scratch; + @Nullable private final byte[] scratch; - private AesFlushingCipher cipher; + @Nullable private AesFlushingCipher cipher; /** * Create an instance whose {@code write} methods have the side effect of overwriting the input @@ -55,7 +58,7 @@ public final class AesCipherDataSink implements DataSink { * cipher calls will be required to complete the operation. If {@code null} then encryption * will overwrite the input {@code data}. */ - public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, byte[] scratch) { + public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, @Nullable byte[] scratch) { this.wrappedDataSink = wrappedDataSink; this.secretKey = secretKey; this.scratch = scratch; @@ -73,15 +76,16 @@ public final class AesCipherDataSink implements DataSink { public void write(byte[] data, int offset, int length) throws IOException { if (scratch == null) { // In-place mode. Writes over the input data. - cipher.updateInPlace(data, offset, length); + castNonNull(cipher).updateInPlace(data, offset, length); wrappedDataSink.write(data, offset, length); } else { // Use scratch space. The original data remains intact. int bytesProcessed = 0; while (bytesProcessed < length) { int bytesToProcess = Math.min(length - bytesProcessed, scratch.length); - cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0); - wrappedDataSink.write(scratch, 0, bytesToProcess); + castNonNull(cipher) + .update(data, offset + bytesProcessed, bytesToProcess, scratch, /* outOffset= */ 0); + wrappedDataSink.write(scratch, /* offset= */ 0, bytesToProcess); bytesProcessed += bytesToProcess; } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java index 7a7af6b8a..0910c63c1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.net.Uri; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -34,7 +36,7 @@ public final class AesCipherDataSource implements DataSource { private final DataSource upstream; private final byte[] secretKey; - private @Nullable AesFlushingCipher cipher; + @Nullable private AesFlushingCipher cipher; public AesCipherDataSource(byte[] secretKey, DataSource upstream) { this.upstream = upstream; @@ -64,12 +66,13 @@ public final class AesCipherDataSource implements DataSource { if (read == C.RESULT_END_OF_INPUT) { return C.RESULT_END_OF_INPUT; } - cipher.updateInPlace(data, offset, read); + castNonNull(cipher).updateInPlace(data, offset, read); return read; } @Override - public @Nullable Uri getUri() { + @Nullable + public Uri getUri() { return upstream.getUri(); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java index ff8841fa9..3418f46ed 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import androidx.annotation.Nullable; + /** * Utility functions for the crypto package. */ @@ -24,10 +26,10 @@ package com.google.android.exoplayer2.upstream.crypto; /** * Returns the hash value of the input as a long using the 64 bit FNV-1a hash function. The hash - * values produced by this function are less likely to collide than those produced by - * {@link #hashCode()}. + * values produced by this function are less likely to collide than those produced by {@link + * #hashCode()}. */ - public static long getFNV64Hash(String input) { + public static long getFNV64Hash(@Nullable String input) { if (input == null) { return 0; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Assertions.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Assertions.java index b4ccc5bcc..0f3bbfa14 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Assertions.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Assertions.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.util; import android.os.Looper; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; @@ -96,6 +96,42 @@ public final class Assertions { } } + /** + * Throws {@link IllegalStateException} if {@code reference} is null. + * + * @param The type of the reference. + * @param reference The reference. + * @return The non-null reference that was validated. + * @throws IllegalStateException If {@code reference} is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkStateNotNull(@Nullable T reference) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new IllegalStateException(); + } + return reference; + } + + /** + * Throws {@link IllegalStateException} if {@code reference} is null. + * + * @param The type of the reference. + * @param reference The 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 IllegalStateException If {@code reference} is null. + */ + @SuppressWarnings({"contracts.postcondition.not.satisfied", "return.type.incompatible"}) + @EnsuresNonNull({"#1"}) + public static T checkStateNotNull(@Nullable T reference, Object errorMessage) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + return reference; + } + /** * Throws {@link NullPointerException} if {@code reference} is null. * diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index 07a8b0a88..fa40f0f01 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.util; -import androidx.annotation.NonNull; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -190,12 +189,12 @@ public final class AtomicFile { } @Override - public void write(@NonNull byte[] b) throws IOException { + public void write(byte[] b) throws IOException { fileOutputStream.write(b); } @Override - public void write(@NonNull byte[] b, int off, int len) throws IOException { + public void write(byte[] b, int off, int len) throws IOException { fileOutputStream.write(b, off, len); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Clock.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Clock.java index 7a87d7d9a..ffb8236bd 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -30,6 +30,13 @@ public interface Clock { */ Clock DEFAULT = new SystemClock(); + /** + * Returns the current time in milliseconds since the Unix Epoch. + * + * @see System#currentTimeMillis() + */ + long currentTimeMillis(); + /** @see android.os.SystemClock#elapsedRealtime() */ long elapsedRealtime(); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index 16a891dbc..3372f2397 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.util; -import androidx.annotation.Nullable; import android.util.Pair; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; import java.util.ArrayList; @@ -83,7 +83,7 @@ public final class CodecSpecificDataUtil { private CodecSpecificDataUtil() {} /** - * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. * @return A pair consisting of the sample rate in Hz and the channel count. @@ -95,7 +95,7 @@ public final class CodecSpecificDataUtil { } /** - * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * Parses an AAC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The * position is advanced to the end of the AudioSpecificConfig. @@ -104,8 +104,8 @@ public final class CodecSpecificDataUtil { * @return A pair consisting of the sample rate in Hz and the channel count. * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray, - boolean forceReadToEnd) throws ParserException { + public static Pair parseAacAudioSpecificConfig( + ParsableBitArray bitArray, boolean forceReadToEnd) throws ParserException { int audioObjectType = getAacAudioObjectType(bitArray); int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); @@ -166,10 +166,10 @@ public final class CodecSpecificDataUtil { * 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. + * @param channelCount The channel count. * @return The AudioSpecificConfig. */ - public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int numChannels) { + public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int channelCount) { int sampleRateIndex = C.INDEX_UNSET; for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) { if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) { @@ -178,13 +178,13 @@ public final class CodecSpecificDataUtil { } int channelConfig = C.INDEX_UNSET; for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) { - if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) { + if (channelCount == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) { channelConfig = i; } } if (sampleRate == C.INDEX_UNSET || channelConfig == C.INDEX_UNSET) { - throw new IllegalArgumentException("Invalid sample rate or number of channels: " - + sampleRate + ", " + numChannels); + throw new IllegalArgumentException( + "Invalid sample rate or number of channels: " + sampleRate + ", " + channelCount); } return buildAacAudioSpecificConfig(AUDIO_OBJECT_TYPE_AAC_LC, sampleRateIndex, channelConfig); } @@ -205,6 +205,22 @@ public final class CodecSpecificDataUtil { return specificConfig; } + /** + * Parses an ALAC AudioSpecificConfig (i.e. an ALACSpecificConfig). + * + * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. + * @return A pair consisting of the sample rate in Hz and the channel count. + */ + public static Pair parseAlacAudioSpecificConfig(byte[] audioSpecificConfig) { + ParsableByteArray byteArray = new ParsableByteArray(audioSpecificConfig); + byteArray.setPosition(9); + int channelCount = byteArray.readUnsignedByte(); + byteArray.setPosition(20); + int sampleRate = byteArray.readUnsignedIntToInt(); + return Pair.create(sampleRate, channelCount); + } + /** * Builds an RFC 6381 AVC codec string using the provided parameters. * diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index 058a5d6dd..69782ab1e 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -16,13 +16,39 @@ package com.google.android.exoplayer2.util; /** - * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return - * whether they resulted in a change of state. + * An interruptible condition variable. This class provides a number of benefits over {@link + * android.os.ConditionVariable}: + * + *

    + *
  • Consistent use of ({@link Clock#elapsedRealtime()} for timing {@link #block(long)} timeout + * intervals. {@link android.os.ConditionVariable} used {@link System#currentTimeMillis()} + * prior to Android 10, which is not a correct clock to use for interval timing because it's + * not guaranteed to be monotonic. + *
  • Support for injecting a custom {@link Clock}. + *
  • The ability to query the variable's current state, by calling {@link #isOpen()}. + *
  • {@link #open()} and {@link #close()} return whether they changed the variable's state. + *
*/ public final class ConditionVariable { + private final Clock clock; private boolean isOpen; + /** Creates an instance using {@link Clock#DEFAULT}. */ + public ConditionVariable() { + this(Clock.DEFAULT); + } + + /** + * Creates an instance. + * + * @param clock The {@link Clock} whose {@link Clock#elapsedRealtime()} method is used to + * determine when {@link #block(long)} should time out. + */ + public ConditionVariable(Clock clock) { + this.clock = clock; + } + /** * Opens the condition and releases all threads that are blocked. * @@ -60,20 +86,33 @@ public final class ConditionVariable { } /** - * Blocks until the condition is opened or until {@code timeout} milliseconds have passed. + * Blocks until the condition is opened or until {@code timeoutMs} have passed. * - * @param timeout The maximum time to wait in milliseconds. + * @param timeoutMs The maximum time to wait in milliseconds. If {@code timeoutMs <= 0} then the + * call will return immediately without blocking. * @return True if the condition was opened, false if the call returns because of the timeout. * @throws InterruptedException If the thread is interrupted. */ - public synchronized boolean block(long timeout) throws InterruptedException { - long now = android.os.SystemClock.elapsedRealtime(); - long end = now + timeout; - while (!isOpen && now < end) { - wait(end - now); - now = android.os.SystemClock.elapsedRealtime(); + public synchronized boolean block(long timeoutMs) throws InterruptedException { + if (timeoutMs <= 0) { + return isOpen; + } + long nowMs = clock.elapsedRealtime(); + long endMs = nowMs + timeoutMs; + if (endMs < nowMs) { + // timeoutMs is large enough for (nowMs + timeoutMs) to rollover. Block indefinitely. + block(); + } else { + while (!isOpen && nowMs < endMs) { + wait(endMs - nowMs); + nowMs = clock.elapsedRealtime(); + } } return isOpen; } + /** Returns whether the condition is opened. */ + public synchronized boolean isOpen() { + return isOpen; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java index 33b50934f..e72e72c3c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java @@ -83,12 +83,12 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL private final Handler handler; private final int[] textureIdHolder; - private final @Nullable TextureImageListener callback; + @Nullable private final TextureImageListener callback; - private @Nullable EGLDisplay display; - private @Nullable EGLContext context; - private @Nullable EGLSurface surface; - private @Nullable SurfaceTexture texture; + @Nullable private EGLDisplay display; + @Nullable private EGLContext context; + @Nullable private EGLSurface surface; + @Nullable private SurfaceTexture texture; /** * @param handler The {@link Handler} that will be used to call {@link diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index bb3dc8b83..9d145caee 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -16,16 +16,20 @@ package com.google.android.exoplayer2.util; import android.os.SystemClock; -import androidx.annotation.Nullable; +import android.text.TextUtils; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererCapabilities.AdaptiveSupport; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; +import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; @@ -54,7 +58,7 @@ public class EventLogger implements AnalyticsListener { TIME_FORMAT.setGroupingUsed(false); } - private final @Nullable MappingTrackSelector trackSelector; + @Nullable private final MappingTrackSelector trackSelector; private final String tag; private final Timeline.Window window; private final Timeline.Period period; @@ -93,10 +97,25 @@ public class EventLogger implements AnalyticsListener { } @Override - public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady, int state) { + public void onPlayerStateChanged( + EventTime eventTime, boolean playWhenReady, @Player.State int state) { logd(eventTime, "state", playWhenReady + ", " + getStateString(state)); } + @Override + public void onPlaybackSuppressionReasonChanged( + EventTime eventTime, @PlaybackSuppressionReason int playbackSuppressionReason) { + logd( + eventTime, + "playbackSuppressionReason", + getPlaybackSuppressionReasonString(playbackSuppressionReason)); + } + + @Override + public void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) { + logd(eventTime, "isPlaying", Boolean.toString(isPlaying)); + } + @Override public void onRepeatModeChanged(EventTime eventTime, @Player.RepeatMode int repeatMode) { logd(eventTime, "repeatMode", getRepeatModeString(repeatMode)); @@ -133,7 +152,7 @@ public class EventLogger implements AnalyticsListener { int periodCount = eventTime.timeline.getPeriodCount(); int windowCount = eventTime.timeline.getWindowCount(); logd( - "timelineChanged [" + "timeline [" + getEventTimeString(eventTime) + ", periodCount=" + periodCount @@ -177,10 +196,10 @@ public class EventLogger implements AnalyticsListener { MappedTrackInfo mappedTrackInfo = trackSelector != null ? trackSelector.getCurrentMappedTrackInfo() : null; if (mappedTrackInfo == null) { - logd(eventTime, "tracksChanged", "[]"); + logd(eventTime, "tracks", "[]"); return; } - logd("tracksChanged [" + getEventTimeString(eventTime) + ", "); + logd("tracks [" + getEventTimeString(eventTime)); // Log tracks associated to renderers. int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { @@ -193,12 +212,13 @@ public class EventLogger implements AnalyticsListener { String adaptiveSupport = getAdaptiveSupportString( trackGroup.length, - mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); + mappedTrackInfo.getAdaptiveSupport( + rendererIndex, groupIndex, /* includeCapabilitiesExceededTracks= */ false)); logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); String formatSupport = - getFormatSupportString( + RendererCapabilities.getFormatSupportString( mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)); logd( " " @@ -237,7 +257,8 @@ public class EventLogger implements AnalyticsListener { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); String formatSupport = - getFormatSupportString(RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); + RendererCapabilities.getFormatSupportString( + RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); logd( " " + status @@ -262,14 +283,14 @@ public class EventLogger implements AnalyticsListener { @Override public void onMetadata(EventTime eventTime, Metadata metadata) { - logd("metadata [" + getEventTimeString(eventTime) + ", "); + logd("metadata [" + getEventTimeString(eventTime)); printMetadata(metadata, " "); logd("]"); } @Override public void onDecoderEnabled(EventTime eventTime, int trackType, DecoderCounters counters) { - logd(eventTime, "decoderEnabled", getTrackTypeString(trackType)); + logd(eventTime, "decoderEnabled", Util.getTrackTypeString(trackType)); } @Override @@ -277,23 +298,42 @@ public class EventLogger implements AnalyticsListener { logd(eventTime, "audioSessionId", Integer.toString(audioSessionId)); } + @Override + public void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) { + logd( + eventTime, + "audioAttributes", + audioAttributes.contentType + + "," + + audioAttributes.flags + + "," + + audioAttributes.usage + + "," + + audioAttributes.allowedCapturePolicy); + } + + @Override + public void onVolumeChanged(EventTime eventTime, float volume) { + logd(eventTime, "volume", Float.toString(volume)); + } + @Override public void onDecoderInitialized( EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) { - logd(eventTime, "decoderInitialized", getTrackTypeString(trackType) + ", " + decoderName); + logd(eventTime, "decoderInitialized", Util.getTrackTypeString(trackType) + ", " + decoderName); } @Override public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) { logd( eventTime, - "decoderInputFormatChanged", - getTrackTypeString(trackType) + ", " + Format.toLogString(format)); + "decoderInputFormat", + Util.getTrackTypeString(trackType) + ", " + Format.toLogString(format)); } @Override public void onDecoderDisabled(EventTime eventTime, int trackType, DecoderCounters counters) { - logd(eventTime, "decoderDisabled", getTrackTypeString(trackType)); + logd(eventTime, "decoderDisabled", Util.getTrackTypeString(trackType)); } @Override @@ -318,7 +358,7 @@ public class EventLogger implements AnalyticsListener { int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - logd(eventTime, "videoSizeChanged", width + ", " + height); + logd(eventTime, "videoSize", width + ", " + height); } @Override @@ -377,7 +417,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) { - logd(eventTime, "surfaceSizeChanged", width + ", " + height); + logd(eventTime, "surfaceSize", width + ", " + height); } @Override @@ -387,7 +427,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { - logd(eventTime, "downstreamFormatChanged", Format.toLogString(mediaLoadData.trackFormat)); + logd(eventTime, "downstreamFormat", Format.toLogString(mediaLoadData.trackFormat)); } @Override @@ -430,27 +470,26 @@ public class EventLogger implements AnalyticsListener { } /** - * Logs an error message and exception. + * Logs an error message. * * @param msg The message to log. - * @param tr The exception to log. */ - protected void loge(String msg, @Nullable Throwable tr) { - Log.e(tag, msg, tr); + protected void loge(String msg) { + Log.e(tag, msg); } // Internal methods private void logd(EventTime eventTime, String eventName) { - logd(getEventString(eventTime, eventName)); + logd(getEventString(eventTime, eventName, /* eventDescription= */ null, /* throwable= */ null)); } private void logd(EventTime eventTime, String eventName, String eventDescription) { - logd(getEventString(eventTime, eventName, eventDescription)); + logd(getEventString(eventTime, eventName, eventDescription, /* throwable= */ null)); } private void loge(EventTime eventTime, String eventName, @Nullable Throwable throwable) { - loge(getEventString(eventTime, eventName), throwable); + loge(getEventString(eventTime, eventName, /* eventDescription= */ null, throwable)); } private void loge( @@ -458,7 +497,7 @@ public class EventLogger implements AnalyticsListener { String eventName, String eventDescription, @Nullable Throwable throwable) { - loge(getEventString(eventTime, eventName, eventDescription), throwable); + loge(getEventString(eventTime, eventName, eventDescription, throwable)); } private void printInternalError(EventTime eventTime, String type, Exception e) { @@ -471,12 +510,21 @@ public class EventLogger implements AnalyticsListener { } } - private String getEventString(EventTime eventTime, String eventName) { - return eventName + " [" + getEventTimeString(eventTime) + "]"; - } - - private String getEventString(EventTime eventTime, String eventName, String eventDescription) { - return eventName + " [" + getEventTimeString(eventTime) + ", " + eventDescription + "]"; + private String getEventString( + EventTime eventTime, + String eventName, + @Nullable String eventDescription, + @Nullable Throwable throwable) { + String eventString = eventName + " [" + getEventTimeString(eventTime); + if (eventDescription != null) { + eventString += ", " + eventDescription; + } + @Nullable String throwableString = Log.getThrowableString(throwable); + if (!TextUtils.isEmpty(throwableString)) { + eventString += "\n " + throwableString.replace("\n", "\n ") + '\n'; + } + eventString += "]"; + return eventString; } private String getEventTimeString(EventTime eventTime) { @@ -489,8 +537,9 @@ public class EventLogger implements AnalyticsListener { windowPeriodString += ", ad=" + eventTime.mediaPeriodId.adIndexInAdGroup; } } - return getTimeString(eventTime.realtimeMs - startTimeMs) - + ", " + return "eventTime=" + + getTimeString(eventTime.realtimeMs - startTimeMs) + + ", mediaPos=" + getTimeString(eventTime.currentPlaybackPositionMs) + ", " + windowPeriodString; @@ -515,24 +564,8 @@ public class EventLogger implements AnalyticsListener { } } - private static String getFormatSupportString(int formatSupport) { - switch (formatSupport) { - case RendererCapabilities.FORMAT_HANDLED: - return "YES"; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - return "NO_EXCEEDS_CAPABILITIES"; - case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: - return "NO_UNSUPPORTED_DRM"; - case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: - return "NO_UNSUPPORTED_TYPE"; - case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: - return "NO"; - default: - return "?"; - } - } - - private static String getAdaptiveSupportString(int trackCount, int adaptiveSupport) { + private static String getAdaptiveSupportString( + int trackCount, @AdaptiveSupport int adaptiveSupport) { if (trackCount < 2) { return "N/A"; } @@ -544,7 +577,7 @@ public class EventLogger implements AnalyticsListener { case RendererCapabilities.ADAPTIVE_NOT_SUPPORTED: return "NO"; default: - return "?"; + throw new IllegalStateException(); } } @@ -604,24 +637,15 @@ public class EventLogger implements AnalyticsListener { } } - private static String getTrackTypeString(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_AUDIO: - return "audio"; - case C.TRACK_TYPE_DEFAULT: - return "default"; - case C.TRACK_TYPE_METADATA: - return "metadata"; - case C.TRACK_TYPE_CAMERA_MOTION: - return "camera motion"; - case C.TRACK_TYPE_NONE: - return "none"; - case C.TRACK_TYPE_TEXT: - return "text"; - case C.TRACK_TYPE_VIDEO: - return "video"; + private static String getPlaybackSuppressionReasonString( + @PlaybackSuppressionReason int playbackSuppressionReason) { + switch (playbackSuppressionReason) { + case Player.PLAYBACK_SUPPRESSION_REASON_NONE: + return "NONE"; + case Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS: + return "TRANSIENT_AUDIO_FOCUS_LOSS"; default: - return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; + return "?"; } } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java new file mode 100644 index 000000000..0d36d78ff --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/FlacConstants.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +/** Defines constants used by the FLAC extractor. */ +public final class FlacConstants { + + /** Size of the FLAC stream marker in bytes. */ + public static final int STREAM_MARKER_SIZE = 4; + /** Size of the header of a FLAC metadata block in bytes. */ + public static final int METADATA_BLOCK_HEADER_SIZE = 4; + /** Size of the FLAC stream info block (header included) in bytes. */ + public static final int STREAM_INFO_BLOCK_SIZE = 38; + /** Minimum size of a FLAC frame header in bytes. */ + public static final int MIN_FRAME_HEADER_SIZE = 6; + /** Maximum size of a FLAC frame header in bytes. */ + public static final int MAX_FRAME_HEADER_SIZE = 16; + + /** Stream info metadata block type. */ + public static final int METADATA_TYPE_STREAM_INFO = 0; + /** Seek table metadata block type. */ + public static final int METADATA_TYPE_SEEK_TABLE = 3; + /** Vorbis comment metadata block type. */ + public static final int METADATA_TYPE_VORBIS_COMMENT = 4; + /** Picture metadata block type. */ + public static final int METADATA_TYPE_PICTURE = 6; + + private FlacConstants() {} +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java index 2c814294a..470e82c13 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/FlacStreamMetadata.java @@ -17,115 +17,189 @@ package com.google.android.exoplayer2.util; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.flac.PictureFrame; import com.google.android.exoplayer2.metadata.flac.VorbisComment; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -/** Holder for FLAC metadata. */ +/** + * Holder for FLAC metadata. + * + * @see FLAC format + * METADATA_BLOCK_STREAMINFO + * @see FLAC format + * METADATA_BLOCK_SEEKTABLE + * @see FLAC format + * METADATA_BLOCK_VORBIS_COMMENT + * @see FLAC format + * METADATA_BLOCK_PICTURE + */ public final class FlacStreamMetadata { + /** A FLAC seek table. */ + public static class SeekTable { + /** Seek points sample numbers. */ + public final long[] pointSampleNumbers; + /** Seek points byte offsets from the first frame. */ + public final long[] pointOffsets; + + public SeekTable(long[] pointSampleNumbers, long[] pointOffsets) { + this.pointSampleNumbers = pointSampleNumbers; + this.pointOffsets = pointOffsets; + } + } + private static final String TAG = "FlacStreamMetadata"; - 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; - @Nullable public final Metadata metadata; - + /** Indicates that a value is not in the corresponding lookup table. */ + public static final int NOT_IN_LOOKUP_TABLE = -1; + /** Separator between the field name of a Vorbis comment and the corresponding value. */ private static final String SEPARATOR = "="; + /** Minimum number of samples per block. */ + public final int minBlockSizeSamples; + /** Maximum number of samples per block. */ + public final int maxBlockSizeSamples; + /** Minimum frame size in bytes, or 0 if the value is unknown. */ + public final int minFrameSize; + /** Maximum frame size in bytes, or 0 if the value is unknown. */ + public final int maxFrameSize; + /** Sample rate in Hertz. */ + public final int sampleRate; + /** + * Lookup key corresponding to the stream sample rate, or {@link #NOT_IN_LOOKUP_TABLE} if it is + * not in the lookup table. + * + *

This key is used to indicate the sample rate in the frame header for the most common values. + * + *

The sample rate lookup table is described in https://xiph.org/flac/format.html#frame_header. + */ + public final int sampleRateLookupKey; + /** Number of audio channels. */ + public final int channels; + /** Number of bits per sample. */ + public final int bitsPerSample; + /** + * Lookup key corresponding to the number of bits per sample of the stream, or {@link + * #NOT_IN_LOOKUP_TABLE} if it is not in the lookup table. + * + *

This key is used to indicate the number of bits per sample in the frame header for the most + * common values. + * + *

The sample size lookup table is described in https://xiph.org/flac/format.html#frame_header. + */ + public final int bitsPerSampleLookupKey; + /** Total number of samples, or 0 if the value is unknown. */ + public final long totalSamples; + /** Seek table, or {@code null} if it is not provided. */ + @Nullable public final SeekTable seekTable; + /** Content metadata, or {@code null} if it is not provided. */ + @Nullable private final Metadata metadata; + /** * Parses binary FLAC stream info metadata. * - * @param data An array containing binary FLAC stream info metadata. - * @param offset The offset of the stream info metadata in {@code data}. - * @see FLAC format - * METADATA_BLOCK_STREAMINFO + * @param data An array containing binary FLAC stream info block. + * @param offset The offset of the stream info block in {@code data}, excluding the header (i.e. + * the offset points to the first byte of the minimum block size). */ public FlacStreamMetadata(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(4) & 0xFL) << 32) | (scratch.readBits(32) & 0xFFFFFFFFL); - this.metadata = null; + minBlockSizeSamples = scratch.readBits(16); + maxBlockSizeSamples = scratch.readBits(16); + minFrameSize = scratch.readBits(24); + maxFrameSize = scratch.readBits(24); + sampleRate = scratch.readBits(20); + sampleRateLookupKey = getSampleRateLookupKey(sampleRate); + channels = scratch.readBits(3) + 1; + bitsPerSample = scratch.readBits(5) + 1; + bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample); + totalSamples = scratch.readBitsToLong(36); + seekTable = null; + metadata = null; } - /** - * @param minBlockSize Minimum block size of the FLAC stream. - * @param maxBlockSize Maximum block size of the FLAC stream. - * @param minFrameSize Minimum frame size of the FLAC stream. - * @param maxFrameSize Maximum frame size of the FLAC stream. - * @param sampleRate Sample rate of the FLAC stream. - * @param channels Number of channels of the FLAC stream. - * @param bitsPerSample Number of bits per sample of the FLAC stream. - * @param totalSamples Total samples of the FLAC stream. - * @param vorbisComments Vorbis comments. Each entry must be in key=value form. - * @param pictureFrames Picture frames. - * @see FLAC format - * METADATA_BLOCK_STREAMINFO - * @see FLAC format - * METADATA_BLOCK_VORBIS_COMMENT - * @see FLAC format - * METADATA_BLOCK_PICTURE - */ + // Used in native code. public FlacStreamMetadata( - int minBlockSize, - int maxBlockSize, + int minBlockSizeSamples, + int maxBlockSizeSamples, int minFrameSize, int maxFrameSize, int sampleRate, int channels, int bitsPerSample, long totalSamples, - List vorbisComments, - List pictureFrames) { - this.minBlockSize = minBlockSize; - this.maxBlockSize = maxBlockSize; + ArrayList vorbisComments, + ArrayList pictureFrames) { + this( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + /* seekTable= */ null, + buildMetadata(vorbisComments, pictureFrames)); + } + + private FlacStreamMetadata( + int minBlockSizeSamples, + int maxBlockSizeSamples, + int minFrameSize, + int maxFrameSize, + int sampleRate, + int channels, + int bitsPerSample, + long totalSamples, + @Nullable SeekTable seekTable, + @Nullable Metadata metadata) { + this.minBlockSizeSamples = minBlockSizeSamples; + this.maxBlockSizeSamples = maxBlockSizeSamples; this.minFrameSize = minFrameSize; this.maxFrameSize = maxFrameSize; this.sampleRate = sampleRate; + this.sampleRateLookupKey = getSampleRateLookupKey(sampleRate); this.channels = channels; this.bitsPerSample = bitsPerSample; + this.bitsPerSampleLookupKey = getBitsPerSampleLookupKey(bitsPerSample); this.totalSamples = totalSamples; - this.metadata = buildMetadata(vorbisComments, pictureFrames); + this.seekTable = seekTable; + this.metadata = metadata; } /** Returns the maximum size for a decoded frame from the FLAC stream. */ - public int maxDecodedFrameSize() { - return maxBlockSize * channels * (bitsPerSample / 8); + public int getMaxDecodedFrameSize() { + return maxBlockSizeSamples * channels * (bitsPerSample / 8); } /** Returns the bit-rate of the FLAC stream. */ - public int bitRate() { - return bitsPerSample * sampleRate; - } - - /** Returns the duration of the FLAC stream in microseconds. */ - public long durationUs() { - return (totalSamples * 1000000L) / sampleRate; + public int getBitRate() { + return bitsPerSample * sampleRate * channels; } /** - * Returns the sample index for the sample at given position. + * Returns the duration of the FLAC stream in microseconds, or {@link C#TIME_UNSET} if the total + * number of samples if unknown. + */ + public long getDurationUs() { + return totalSamples == 0 ? C.TIME_UNSET : totalSamples * C.MICROS_PER_SECOND / sampleRate; + } + + /** + * Returns the sample number of the sample at a given time. * * @param timeUs Time position in microseconds in the FLAC stream. - * @return The sample index for the sample at given position. + * @return The sample number corresponding to the time position. */ - public long getSampleIndex(long timeUs) { - long sampleIndex = (timeUs * sampleRate) / C.MICROS_PER_SECOND; - return Util.constrainValue(sampleIndex, 0, totalSamples - 1); + public long getSampleNumber(long timeUs) { + long sampleNumber = (timeUs * sampleRate) / C.MICROS_PER_SECOND; + return Util.constrainValue(sampleNumber, /* min= */ 0, totalSamples - 1); } /** Returns the approximate number of bytes per frame for the current FLAC stream. */ @@ -136,12 +210,155 @@ public final class FlacStreamMetadata { } else { // Uses the stream's block-size if it's a known fixed block-size stream, otherwise uses the // default value for FLAC block-size, which is 4096. - long blockSize = (minBlockSize == maxBlockSize && minBlockSize > 0) ? minBlockSize : 4096; - approxBytesPerFrame = (blockSize * channels * bitsPerSample) / 8 + 64; + long blockSizeSamples = + (minBlockSizeSamples == maxBlockSizeSamples && minBlockSizeSamples > 0) + ? minBlockSizeSamples + : 4096; + approxBytesPerFrame = (blockSizeSamples * channels * bitsPerSample) / 8 + 64; } return approxBytesPerFrame; } + /** + * Returns a {@link Format} extracted from the FLAC stream metadata. + * + *

{@code streamMarkerAndInfoBlock} is updated to set the bit corresponding to the stream info + * last metadata block flag to true. + * + * @param streamMarkerAndInfoBlock An array containing the FLAC stream marker followed by the + * stream info block. + * @param id3Metadata The ID3 metadata of the stream, or {@code null} if there is no such data. + * @return The extracted {@link Format}. + */ + public Format getFormat(byte[] streamMarkerAndInfoBlock, @Nullable Metadata id3Metadata) { + // Set the last metadata block flag, ignore the other blocks. + streamMarkerAndInfoBlock[4] = (byte) 0x80; + int maxInputSize = maxFrameSize > 0 ? maxFrameSize : Format.NO_VALUE; + @Nullable Metadata metadataWithId3 = getMetadataCopyWithAppendedEntriesFrom(id3Metadata); + + return Format.createAudioSampleFormat( + /* id= */ null, + MimeTypes.AUDIO_FLAC, + /* codecs= */ null, + getBitRate(), + maxInputSize, + channels, + sampleRate, + /* pcmEncoding= */ Format.NO_VALUE, + /* encoderDelay= */ 0, + /* encoderPadding= */ 0, + /* initializationData= */ Collections.singletonList(streamMarkerAndInfoBlock), + /* drmInitData= */ null, + /* selectionFlags= */ 0, + /* language= */ null, + metadataWithId3); + } + + /** Returns a copy of the content metadata with entries from {@code other} appended. */ + @Nullable + public Metadata getMetadataCopyWithAppendedEntriesFrom(@Nullable Metadata other) { + return metadata == null ? other : metadata.copyWithAppendedEntriesFrom(other); + } + + /** Returns a copy of {@code this} with the seek table replaced by the one given. */ + public FlacStreamMetadata copyWithSeekTable(@Nullable SeekTable seekTable) { + return new FlacStreamMetadata( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + seekTable, + metadata); + } + + /** Returns a copy of {@code this} with the given Vorbis comments added to the metadata. */ + public FlacStreamMetadata copyWithVorbisComments(List vorbisComments) { + @Nullable + Metadata appendedMetadata = + getMetadataCopyWithAppendedEntriesFrom( + buildMetadata(vorbisComments, Collections.emptyList())); + return new FlacStreamMetadata( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + seekTable, + appendedMetadata); + } + + /** Returns a copy of {@code this} with the given picture frames added to the metadata. */ + public FlacStreamMetadata copyWithPictureFrames(List pictureFrames) { + @Nullable + Metadata appendedMetadata = + getMetadataCopyWithAppendedEntriesFrom( + buildMetadata(Collections.emptyList(), pictureFrames)); + return new FlacStreamMetadata( + minBlockSizeSamples, + maxBlockSizeSamples, + minFrameSize, + maxFrameSize, + sampleRate, + channels, + bitsPerSample, + totalSamples, + seekTable, + appendedMetadata); + } + + private static int getSampleRateLookupKey(int sampleRate) { + switch (sampleRate) { + case 88200: + return 1; + case 176400: + return 2; + case 192000: + return 3; + case 8000: + return 4; + case 16000: + return 5; + case 22050: + return 6; + case 24000: + return 7; + case 32000: + return 8; + case 44100: + return 9; + case 48000: + return 10; + case 96000: + return 11; + default: + return NOT_IN_LOOKUP_TABLE; + } + } + + private static int getBitsPerSampleLookupKey(int bitsPerSample) { + switch (bitsPerSample) { + case 8: + return 1; + case 12: + return 2; + case 16: + return 4; + case 20: + return 5; + case 24: + return 6; + default: + return NOT_IN_LOOKUP_TABLE; + } + } + @Nullable private static Metadata buildMetadata( List vorbisComments, List pictureFrames) { @@ -154,7 +371,7 @@ public final class FlacStreamMetadata { String vorbisComment = vorbisComments.get(i); String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR); if (keyAndValue.length != 2) { - Log.w(TAG, "Failed to parse vorbis comment: " + vorbisComment); + Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment); } else { VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]); metadataEntries.add(entry); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index c7feff516..cc4866118 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -17,23 +17,235 @@ package com.google.android.exoplayer2.util; import static android.opengl.GLU.gluErrorString; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.opengl.EGL14; +import android.opengl.EGLDisplay; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import javax.microedition.khronos.egl.EGL10; -/** GL utility methods. */ +/** GL utilities. */ public final class GlUtil { + + /** + * GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}. + */ + public static final class Attribute { + + /** The name of the attribute in the GLSL sources. */ + public final String name; + + private final int index; + private final int location; + + @Nullable private Buffer buffer; + private int size; + + /** + * Creates a new GL attribute. + * + * @param program The identifier of a compiled and linked GLSL shader program. + * @param index The index of the attribute. After this instance has been constructed, the name + * of the attribute is available via the {@link #name} field. + */ + public Attribute(int program, int index) { + int[] len = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, len, 0); + + int[] type = new int[1]; + int[] size = new int[1]; + byte[] nameBytes = new byte[len[0]]; + int[] ignore = new int[1]; + + GLES20.glGetActiveAttrib(program, index, len[0], ignore, 0, size, 0, type, 0, nameBytes, 0); + name = new String(nameBytes, 0, strlen(nameBytes)); + location = GLES20.glGetAttribLocation(program, name); + this.index = index; + } + + /** + * Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size} + * elements) to this {@link Attribute}. + * + * @param buffer Buffer to bind to this attribute. + * @param size Number of elements per vertex. + */ + public void setBuffer(float[] buffer, int size) { + this.buffer = createBuffer(buffer); + this.size = size; + } + + /** + * Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}. + * + *

Should be called before each drawing call. + */ + public void bind() { + Buffer buffer = Assertions.checkNotNull(this.buffer, "call setBuffer before bind"); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + GLES20.glVertexAttribPointer( + location, + size, // count + GLES20.GL_FLOAT, // type + false, // normalize + 0, // stride + buffer); + GLES20.glEnableVertexAttribArray(index); + checkGlError(); + } + } + + /** + * GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}. + */ + public static final class Uniform { + + /** The name of the uniform in the GLSL sources. */ + public final String name; + + private final int location; + private final int type; + private final float[] value; + + private int texId; + private int unit; + + /** + * Creates a new GL uniform. + * + * @param program The identifier of a compiled and linked GLSL shader program. + * @param index The index of the uniform. After this instance has been constructed, the name of + * the uniform is available via the {@link #name} field. + */ + public Uniform(int program, int index) { + int[] len = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, len, 0); + + int[] type = new int[1]; + int[] size = new int[1]; + byte[] name = new byte[len[0]]; + int[] ignore = new int[1]; + + GLES20.glGetActiveUniform(program, index, len[0], ignore, 0, size, 0, type, 0, name, 0); + this.name = new String(name, 0, strlen(name)); + location = GLES20.glGetUniformLocation(program, this.name); + this.type = type[0]; + + value = new float[1]; + } + + /** + * Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform. + * + * @param texId The GL texture identifier from which to sample. + * @param unit The GL texture unit index. + */ + public void setSamplerTexId(int texId, int unit) { + this.texId = texId; + this.unit = unit; + } + + /** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */ + public void setFloat(float value) { + this.value[0] = value; + } + + /** + * Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)} or + * {@link #setFloat(float)}. + * + *

Should be called before each drawing call. + */ + public void bind() { + if (type == GLES20.GL_FLOAT) { + GLES20.glUniform1fv(location, 1, value, 0); + checkGlError(); + return; + } + + if (texId == 0) { + throw new IllegalStateException("call setSamplerTexId before bind"); + } + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit); + if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) { + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId); + } else if (type == GLES20.GL_SAMPLER_2D) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId); + } else { + throw new IllegalStateException("unexpected uniform type: " + type); + } + GLES20.glUniform1i(location, unit); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + checkGlError(); + } + } + private static final String TAG = "GlUtil"; + private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; + private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; + /** Class only contains static methods. */ private GlUtil() {} + /** + * Returns whether creating a GL context with {@value EXTENSION_PROTECTED_CONTENT} is possible. If + * {@code true}, the device supports a protected output path for DRM content when using GL. + */ + @TargetApi(24) + public static boolean isProtectedContentExtensionSupported(Context context) { + if (Util.SDK_INT < 24) { + return false; + } + if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { + // Samsung devices running Nougat are known to be broken. See + // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. + // Moto Z XT1650 is also affected. See + // https://github.com/google/ExoPlayer/issues/3215. + return false; + } + if (Util.SDK_INT < 26 + && !context + .getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { + // Pre API level 26 devices were not well tested unless they supported VR mode. + return false; + } + + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + return eglExtensions != null && eglExtensions.contains(EXTENSION_PROTECTED_CONTENT); + } + + /** + * Returns whether creating a GL context with {@value EXTENSION_SURFACELESS_CONTEXT} is possible. + */ + @TargetApi(17) + public static boolean isSurfacelessContextExtensionSupported() { + if (Util.SDK_INT < 17) { + return false; + } + EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + return eglExtensions != null && eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT); + } + /** * If there is an OpenGl error, logs the error and if {@link * ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}. @@ -90,6 +302,34 @@ public final class GlUtil { return program; } + /** Returns the {@link Attribute}s in the specified {@code program}. */ + public static Attribute[] getAttributes(int program) { + int[] attributeCount = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0); + if (attributeCount[0] != 2) { + throw new IllegalStateException("expected two attributes"); + } + + Attribute[] attributes = new Attribute[attributeCount[0]]; + for (int i = 0; i < attributeCount[0]; i++) { + attributes[i] = new Attribute(program, i); + } + return attributes; + } + + /** Returns the {@link Uniform}s in the specified {@code program}. */ + public static Uniform[] getUniforms(int program) { + int[] uniformCount = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0); + + Uniform[] uniforms = new Uniform[uniformCount[0]]; + for (int i = 0; i < uniformCount[0]; i++) { + uniforms[i] = new Uniform(program, i); + } + + return uniforms; + } + /** * Allocates a FloatBuffer with the given data. * @@ -151,4 +391,14 @@ public final class GlUtil { throw new RuntimeException(errorMsg); } } + + /** Returns the length of the null-terminated string in {@code strVal}. */ + private static int strlen(byte[] strVal) { + for (int i = 0; i < strVal.length; ++i) { + if (strVal[i] == '\0') { + return i; + } + } + return strVal.length; + } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index 8f1a6544c..5b85b26c3 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util; import android.os.Handler; import android.os.Looper; import android.os.Message; +import androidx.annotation.Nullable; /** * An interface to call through to a {@link Handler}. Instances must be created by calling {@link @@ -32,13 +33,13 @@ public interface HandlerWrapper { Message obtainMessage(int what); /** @see Handler#obtainMessage(int, Object) */ - Message obtainMessage(int what, Object obj); + Message obtainMessage(int what, @Nullable Object obj); /** @see Handler#obtainMessage(int, int, int) */ Message obtainMessage(int what, int arg1, int arg2); /** @see Handler#obtainMessage(int, int, int, Object) */ - Message obtainMessage(int what, int arg1, int arg2, Object obj); + Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj); /** @see Handler#sendEmptyMessage(int) */ boolean sendEmptyMessage(int what); @@ -50,7 +51,7 @@ public interface HandlerWrapper { void removeMessages(int what); /** @see Handler#removeCallbacksAndMessages(Object) */ - void removeCallbacksAndMessages(Object token); + void removeCallbacksAndMessages(@Nullable Object token); /** @see Handler#post(Runnable) */ boolean post(Runnable runnable); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Log.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Log.java index 1eb097784..e5e6f88d4 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Log.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Log.java @@ -15,12 +15,13 @@ */ package com.google.android.exoplayer2.util; +import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import android.text.TextUtils; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.net.UnknownHostException; /** Wrapper around {@link android.util.Log} which allows to set the log level. */ public final class Log { @@ -69,7 +70,8 @@ public final class Log { } /** - * Sets whether stack traces of {@link Throwable}s will be logged to logcat. + * Sets whether stack traces of {@link Throwable}s will be logged to logcat. Stack trace logging + * is enabled by default. * * @param logStackTraces Whether stack traces will be logged. */ @@ -86,11 +88,7 @@ public final class Log { /** @see android.util.Log#d(String, String, Throwable) */ public static void d(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - d(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel == LOG_LEVEL_ALL) { - android.util.Log.d(tag, message, throwable); - } + d(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#i(String, String) */ @@ -102,11 +100,7 @@ public final class Log { /** @see android.util.Log#i(String, String, Throwable) */ public static void i(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - i(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_INFO) { - android.util.Log.i(tag, message, throwable); - } + i(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#w(String, String) */ @@ -118,11 +112,7 @@ public final class Log { /** @see android.util.Log#w(String, String, Throwable) */ public static void w(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - w(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_WARNING) { - android.util.Log.w(tag, message, throwable); - } + w(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#e(String, String) */ @@ -134,18 +124,54 @@ public final class Log { /** @see android.util.Log#e(String, String, Throwable) */ public static void e(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - e(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_ERROR) { - android.util.Log.e(tag, message, throwable); + e(tag, appendThrowableString(message, throwable)); + } + + /** + * Returns a string representation of a {@link Throwable} suitable for logging, taking into + * account whether {@link #setLogStackTraces(boolean)} stack trace logging} is enabled. + * + *

Stack trace logging may be unconditionally suppressed for some expected failure modes (e.g., + * {@link Throwable Throwables} that are expected if the device doesn't have network connectivity) + * to avoid log spam. + * + * @param throwable The {@link Throwable}. + * @return The string representation of the {@link Throwable}. + */ + @Nullable + public static String getThrowableString(@Nullable Throwable throwable) { + if (throwable == null) { + return null; + } else if (isCausedByUnknownHostException(throwable)) { + // UnknownHostException implies the device doesn't have network connectivity. + // UnknownHostException.getMessage() may return a string that's more verbose than desired for + // logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has + // special handling to return the empty string, which can result in logging that doesn't + // indicate the failure mode at all. Hence we special case this exception to always return a + // concise but useful message. + return "UnknownHostException (no network)"; + } else if (!logStackTraces) { + return throwable.getMessage(); + } else { + return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " "); } } - private static String appendThrowableMessage(String message, @Nullable Throwable throwable) { - if (throwable == null) { - return message; + private static String appendThrowableString(String message, @Nullable Throwable throwable) { + @Nullable String throwableString = getThrowableString(throwable); + if (!TextUtils.isEmpty(throwableString)) { + message += "\n " + throwableString.replace("\n", "\n ") + '\n'; } - String throwableMessage = throwable.getMessage(); - return TextUtils.isEmpty(throwableMessage) ? message : message + " - " + throwableMessage; + return message; + } + + private static boolean isCausedByUnknownHostException(@Nullable Throwable throwable) { + while (throwable != null) { + if (throwable instanceof UnknownHostException) { + return true; + } + throwable = throwable.getCause(); + } + return false; } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/MediaClock.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/MediaClock.java index a10298e45..e9f08a35c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/MediaClock.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/MediaClock.java @@ -28,13 +28,12 @@ public interface MediaClock { long getPositionUs(); /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. + * Attempts to set the playback parameters. The media clock may override these parameters if they + * are not supported. * - * @param playbackParameters The playback parameters. - * @return The active playback parameters. + * @param playbackParameters The playback parameters to attempt to set. */ - PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + void setPlaybackParameters(PlaybackParameters playbackParameters); /** * Returns the active playback parameters. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 61457c308..5465da748 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.util; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import java.util.ArrayList; @@ -122,39 +122,75 @@ public final class MimeTypes { customMimeTypes.add(customMimeType); } - /** Returns whether the given string is an audio mime type. */ + /** Returns whether the given string is an audio MIME type. */ public static boolean isAudio(@Nullable String mimeType) { return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType)); } - /** Returns whether the given string is a video mime type. */ + /** Returns whether the given string is a video MIME type. */ public static boolean isVideo(@Nullable String mimeType) { return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); } - /** Returns whether the given string is a text mime type. */ + /** Returns whether the given string is a text MIME type. */ public static boolean isText(@Nullable String mimeType) { return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType)); } - /** Returns whether the given string is an application mime type. */ + /** Returns whether the given string is an application MIME type. */ public static boolean isApplication(@Nullable String mimeType) { return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); } + /** + * Returns true if it is known that all samples in a stream of the given sample MIME type are + * guaranteed to be sync samples (i.e., {@link C#BUFFER_FLAG_KEY_FRAME} is guaranteed to be set on + * every sample). + * + * @param mimeType The sample MIME type. + * @return True if it is known that all samples in a stream of the given sample MIME type are + * guaranteed to be sync samples. False otherwise, including if {@code null} is passed. + */ + public static boolean allSamplesAreSyncSamples(@Nullable String mimeType) { + if (mimeType == null) { + return false; + } + // TODO: Add additional audio MIME types. Also consider evaluating based on Format rather than + // just MIME type, since in some cases the property is true for a subset of the profiles + // belonging to a single MIME type. If we do this, we should move the method to a different + // class. See [Internal ref: http://go/exo-audio-format-random-access]. + switch (mimeType) { + case AUDIO_MPEG: + case AUDIO_MPEG_L1: + case AUDIO_MPEG_L2: + case AUDIO_RAW: + case AUDIO_ALAW: + case AUDIO_MLAW: + case AUDIO_OPUS: + case AUDIO_FLAC: + case AUDIO_AC3: + case AUDIO_E_AC3: + case AUDIO_E_AC3_JOC: + return true; + default: + return false; + } + } + /** * Derives a video sample mimeType from a codecs attribute. * * @param codecs The codecs attribute. * @return The derived video mimeType, or null if it could not be derived. */ - public static @Nullable String getVideoMediaMimeType(@Nullable String codecs) { + @Nullable + public static String getVideoMediaMimeType(@Nullable String codecs) { if (codecs == null) { return null; } String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isVideo(mimeType)) { return mimeType; } @@ -168,13 +204,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @return The derived audio mimeType, or null if it could not be derived. */ - public static @Nullable String getAudioMediaMimeType(@Nullable String codecs) { + @Nullable + public static String getAudioMediaMimeType(@Nullable String codecs) { if (codecs == null) { return null; } String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isAudio(mimeType)) { return mimeType; } @@ -188,7 +225,8 @@ public final class MimeTypes { * @param codec The codec identifier to derive. * @return The mimeType, or null if it could not be derived. */ - public static @Nullable String getMediaMimeType(@Nullable String codec) { + @Nullable + public static String getMediaMimeType(@Nullable String codec) { if (codec == null) { return null; } @@ -209,7 +247,7 @@ public final class MimeTypes { } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { - String mimeType = null; + @Nullable String mimeType = null; if (codec.startsWith("mp4a.")) { String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix if (objectTypeString.length() >= 2) { @@ -218,7 +256,7 @@ public final class MimeTypes { int objectTypeInt = Integer.parseInt(objectTypeHexString, 16); mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt); } catch (NumberFormatException ignored) { - // ignored + // Ignored. } } } @@ -241,6 +279,10 @@ public final class MimeTypes { return MimeTypes.AUDIO_VORBIS; } else if (codec.startsWith("flac")) { return MimeTypes.AUDIO_FLAC; + } else if (codec.startsWith("stpp")) { + return MimeTypes.APPLICATION_TTML; + } else if (codec.startsWith("wvtt")) { + return MimeTypes.TEXT_VTT; } else { return getCustomMimeTypeForCodec(codec); } @@ -345,6 +387,8 @@ public final class MimeTypes { */ public static @C.Encoding int getEncoding(String mimeType) { switch (mimeType) { + case MimeTypes.AUDIO_MPEG: + return C.ENCODING_MP3; case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: @@ -378,7 +422,8 @@ public final class MimeTypes { * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not * contain a forward slash character ({@code '/'}). */ - private static @Nullable String getTopLevelType(@Nullable String mimeType) { + @Nullable + private static String getTopLevelType(@Nullable String mimeType) { if (mimeType == null) { return null; } @@ -389,7 +434,8 @@ public final class MimeTypes { return mimeType.substring(0, indexOfSlash); } - private static @Nullable String getCustomMimeTypeForCodec(String codec) { + @Nullable + private static String getCustomMimeTypeForCodec(String codec) { int customMimeTypeCount = customMimeTypes.size(); for (int i = 0; i < customMimeTypeCount; i++) { CustomMimeType customMimeType = customMimeTypes.get(i); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java new file mode 100644 index 000000000..694351fc7 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/NonNullApi.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.util; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation to declare all type usages in the annotated instance as {@link Nonnull}, unless + * explicitly marked with a nullable annotation. + */ +@Retention(RetentionPolicy.CLASS) +public @interface NonNullApi {} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 2d8bf95fd..fc1bc653c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -163,7 +163,7 @@ public final class ParsableBitArray { * Reads up to 32 bits. * * @param numBits The number of bits to read. - * @return An integer whose bottom n bits hold the read data. + * @return An integer whose bottom {@code numBits} bits hold the read data. */ public int readBits(int numBits) { if (numBits == 0) { @@ -185,12 +185,25 @@ public final class ParsableBitArray { return returnValue; } + /** + * Reads up to 64 bits. + * + * @param numBits The number of bits to read. + * @return A long whose bottom {@code numBits} bits hold the read data. + */ + public long readBitsToLong(int numBits) { + if (numBits <= 32) { + return Util.toUnsignedLong(readBits(numBits)); + } + return Util.toLong(readBits(numBits - 32), readBits(32)); + } + /** * Reads {@code numBits} bits into {@code buffer}. * - * @param buffer The array into which the read data should be written. The trailing - * {@code numBits % 8} bits are written into the most significant bits of the last modified - * {@code buffer} byte. The remaining ones are unmodified. + * @param buffer The array into which the read data should be written. The trailing {@code numBits + * % 8} bits are written into the most significant bits of the last modified {@code buffer} + * byte. The remaining ones are unmodified. * @param offset The offset in {@code buffer} at which the read data should be written. * @param numBits The number of bits to read. */ diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 0c5116624..67686ad64 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -490,7 +490,8 @@ public final class ParsableByteArray { * @return The string not including any terminating NUL byte, or null if the end of the data has * already been reached. */ - public @Nullable String readNullTerminatedString() { + @Nullable + public String readNullTerminatedString() { if (bytesLeft() == 0) { return null; } @@ -516,7 +517,8 @@ public final class ParsableByteArray { * @return The line not including any line-termination characters, or null if the end of the data * has already been reached. */ - public @Nullable String readLine() { + @Nullable + public String readLine() { if (bytesLeft() == 0) { return null; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java index b1f53416f..e5f9aa645 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/StandaloneMediaClock.java @@ -88,13 +88,12 @@ public final class StandaloneMediaClock implements MediaClock { } @Override - public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(PlaybackParameters playbackParameters) { // Store the current position as the new base, in case the playback speed has changed. if (started) { resetPosition(getPositionUs()); } this.playbackParameters = playbackParameters; - return playbackParameters; } @Override diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index be526595c..89e1c60d7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -21,9 +21,17 @@ import android.os.Looper; import androidx.annotation.Nullable; /** - * The standard implementation of {@link Clock}. + * The standard implementation of {@link Clock}, an instance of which is available via {@link + * SystemClock#DEFAULT}. */ -/* package */ final class SystemClock implements Clock { +public class SystemClock implements Clock { + + protected SystemClock() {} + + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } @Override public long elapsedRealtime() { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java index ee469a5b2..1fbea2ed7 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util; import android.os.Looper; import android.os.Message; +import androidx.annotation.Nullable; /** The standard implementation of {@link HandlerWrapper}. */ /* package */ final class SystemHandlerWrapper implements HandlerWrapper { @@ -38,7 +39,7 @@ import android.os.Message; } @Override - public Message obtainMessage(int what, Object obj) { + public Message obtainMessage(int what, @Nullable Object obj) { return handler.obtainMessage(what, obj); } @@ -48,7 +49,7 @@ import android.os.Message; } @Override - public Message obtainMessage(int what, int arg1, int arg2, Object obj) { + public Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) { return handler.obtainMessage(what, arg1, arg2, obj); } @@ -68,7 +69,7 @@ import android.os.Message; } @Override - public void removeCallbacksAndMessages(Object token) { + public void removeCallbacksAndMessages(@Nullable Object token) { handler.removeCallbacksAndMessages(token); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java index 3ac76eb54..da5d9bafe 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/TimedValueQueue.java @@ -97,7 +97,8 @@ public final class TimedValueQueue { * @return The value with the closest timestamp or null if the buffer is empty or there is no * older value and {@code onlyOlder} is true. */ - private @Nullable V poll(long timestamp, boolean onlyOlder) { + @Nullable + private V poll(long timestamp, boolean onlyOlder) { V value = null; long previousTimeDiff = Long.MAX_VALUE; while (size > 0) { diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/UriUtil.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/UriUtil.java index 071ebf208..90be8660c 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/UriUtil.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/UriUtil.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util; import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.Nullable; /** * Utility methods for manipulating URIs. @@ -69,19 +70,19 @@ public final class UriUtil { * @param baseUri The base URI. * @param referenceUri The reference URI to resolve. */ - public static Uri resolveToUri(String baseUri, String referenceUri) { + public static Uri resolveToUri(@Nullable String baseUri, @Nullable 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. + * + *

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) { + public static String resolve(@Nullable String baseUri, @Nullable String referenceUri) { StringBuilder uri = new StringBuilder(); // Map null onto empty string, to make the following logic simpler. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Util.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Util.java index 144a67029..a7a46b163 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -40,11 +40,12 @@ import android.os.Handler; import android.os.Looper; import android.os.Parcel; import android.security.NetworkSecurityPolicy; -import androidx.annotation.Nullable; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.view.Display; +import android.view.SurfaceView; import android.view.WindowManager; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; @@ -54,8 +55,6 @@ import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.ByteArrayOutputStream; @@ -136,9 +135,8 @@ public final class Util { + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); - // Android standardizes to ISO 639-1 2-letter codes and provides no way to map a 3-letter - // ISO 639-2 code back to the corresponding 2-letter code. - @Nullable private static HashMap languageTagIso3ToIso2; + // Replacement map of ISO language codes used for normalization. + @Nullable private static HashMap languageTagReplacementMap; private Util() {} @@ -146,7 +144,7 @@ public final class Util { * 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. + * method. * @return a byte array containing all of the inputStream's bytes. * @throws IOException if an error occurs reading from the stream. */ @@ -252,14 +250,14 @@ public final class Util { /** * 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. + * + *

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) { + public static boolean contains(@NullableType Object[] items, @Nullable Object item) { for (Object arrayItem : items) { if (areEqual(arrayItem, item)) { return true; @@ -321,7 +319,35 @@ public final class Util { } /** - * Concatenates two non-null type arrays. + * Copies a subset of an array. + * + * @param input The input array. + * @param from The start the range to be copied, inclusive + * @param to The end of the range to be copied, exclusive. + * @return The copied array. + */ + @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:return.type.incompatible"}) + public static T[] nullSafeArrayCopyOfRange(T[] input, int from, int to) { + Assertions.checkArgument(0 <= from); + Assertions.checkArgument(to <= input.length); + return Arrays.copyOfRange(input, from, to); + } + + /** + * Creates a new array containing {@code original} with {@code newElement} appended. + * + * @param original The input array. + * @param newElement The element to append. + * @return The new array. + */ + public static T[] nullSafeArrayAppend(T[] original, T newElement) { + @NullableType T[] result = Arrays.copyOf(original, original.length + 1); + result[original.length] = newElement; + return castNonNullTypeArray(result); + } + + /** + * Creates a new array containing the concatenation of two non-null type arrays. * * @param first The first array. * @param second The second array. @@ -338,7 +364,6 @@ public final class Util { /* length= */ second.length); return concatenation; } - /** * Creates a {@link Handler} with the specified {@link Handler.Callback} on the current {@link * Looper} thread. The method accepts partially initialized objects as callback under the @@ -443,6 +468,20 @@ public final class Util { parcel.writeInt(value ? 1 : 0); } + /** + * Returns the language tag for a {@link Locale}. + * + *

For API levels ≥ 21, this tag is IETF BCP 47 compliant. Use {@link + * #normalizeLanguageCode(String)} to retrieve a normalized IETF BCP 47 language tag for all API + * levels if needed. + * + * @param locale A {@link Locale}. + * @return The language tag. + */ + public static String getLocaleLanguageTag(Locale locale) { + return SDK_INT >= 21 ? getLocaleLanguageTagV21(locale) : locale.toString(); + } + /** * Returns a normalized IETF BCP 47 language tag for {@code language}. * @@ -458,26 +497,23 @@ public final class Util { // Locale data (especially for API < 21) may produce tags with '_' instead of the // standard-conformant '-'. String normalizedTag = language.replace('_', '-'); - if (Util.SDK_INT >= 21) { - // Filters out ill-formed sub-tags, replaces deprecated tags and normalizes all valid tags. - normalizedTag = normalizeLanguageCodeSyntaxV21(normalizedTag); - } if (normalizedTag.isEmpty() || "und".equals(normalizedTag)) { // Tag isn't valid, keep using the original. normalizedTag = language; } normalizedTag = Util.toLowerInvariant(normalizedTag); String mainLanguage = Util.splitAtFirst(normalizedTag, "-")[0]; - if (mainLanguage.length() == 3) { - // 3-letter ISO 639-2/B or ISO 639-2/T language codes will not be converted to 2-letter ISO - // 639-1 codes automatically. - if (languageTagIso3ToIso2 == null) { - languageTagIso3ToIso2 = createIso3ToIso2Map(); - } - String iso2Language = languageTagIso3ToIso2.get(mainLanguage); - if (iso2Language != null) { - normalizedTag = iso2Language + normalizedTag.substring(/* beginIndex= */ 3); - } + if (languageTagReplacementMap == null) { + languageTagReplacementMap = createIsoLanguageReplacementMap(); + } + @Nullable String replacedLanguage = languageTagReplacementMap.get(mainLanguage); + if (replacedLanguage != null) { + normalizedTag = + replacedLanguage + normalizedTag.substring(/* beginIndex= */ mainLanguage.length()); + mainLanguage = replacedLanguage; + } + if ("no".equals(mainLanguage) || "i".equals(mainLanguage) || "zh".equals(mainLanguage)) { + normalizedTag = maybeReplaceGrandfatheredLanguageTags(normalizedTag); } return normalizedTag; } @@ -672,11 +708,47 @@ public final class Util { return result; } + /** + * Returns the index of the first occurrence of {@code value} in {@code array}, or {@link + * C#INDEX_UNSET} if {@code value} is not contained in {@code array}. + * + * @param array The array to search. + * @param value The value to search for. + * @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET} + * if {@code value} is not contained in {@code array}. + */ + public static int linearSearch(int[] array, int value) { + for (int i = 0; i < array.length; i++) { + if (array[i] == value) { + return i; + } + } + return C.INDEX_UNSET; + } + + /** + * Returns the index of the first occurrence of {@code value} in {@code array}, or {@link + * C#INDEX_UNSET} if {@code value} is not contained in {@code array}. + * + * @param array The array to search. + * @param value The value to search for. + * @return The index of the first occurrence of value in {@code array}, or {@link C#INDEX_UNSET} + * if {@code value} is not contained in {@code array}. + */ + public static int linearSearch(long[] array, long value) { + for (int i = 0; i < array.length; i++) { + if (array[i] == value) { + return i; + } + } + return C.INDEX_UNSET; + } + /** * Returns the index of the largest element in {@code array} that is less than (or optionally * equal to) a specified {@code value}. - *

- * The search is performed using a binary search algorithm, so the array must be sorted. If the + * + *

The search is performed using a binary search algorithm, so the array must be sorted. If the * array contains multiple elements equal to {@code value} and {@code inclusive} is true, the * index of the first one will be returned. * @@ -690,8 +762,8 @@ public final class Util { * @return The index of the largest element in {@code array} that is less than (or optionally * equal to) {@code value}. */ - public static int binarySearchFloor(int[] array, int value, boolean inclusive, - boolean stayInBounds) { + public static int binarySearchFloor( + int[] array, int value, boolean inclusive, boolean stayInBounds) { int index = Arrays.binarySearch(array, value); if (index < 0) { index = -(index + 2); @@ -1150,6 +1222,29 @@ public final class Util { return result; } + /** + * Converts an integer to a long by unsigned conversion. + * + *

This method is equivalent to {@link Integer#toUnsignedLong(int)} for API 26+. + */ + public static long toUnsignedLong(int x) { + // x is implicitly casted to a long before the bit operation is executed but this does not + // impact the method correctness. + return x & 0xFFFFFFFFL; + } + + /** + * Return the long that is composed of the bits of the 2 specified integers. + * + * @param mostSignificantBits The 32 most significant bits of the long to return. + * @param leastSignificantBits The 32 least significant bits of the long to return. + * @return a long where its 32 most significant bits are {@code mostSignificantBits} bits and its + * 32 least significant bits are {@code leastSignificantBits}. + */ + public static long toLong(int mostSignificantBits, int leastSignificantBits) { + return (toUnsignedLong(mostSignificantBits) << 32) | toUnsignedLong(leastSignificantBits); + } + /** * Returns a byte array containing values parsed from the hex string provided. * @@ -1210,9 +1305,9 @@ public final class Util { * @param codecs A codec sequence string, as defined in RFC 6381. * @param trackType One of {@link C}{@code .TRACK_TYPE_*}. * @return A copy of {@code codecs} without the codecs whose track type doesn't match {@code - * trackType}. + * trackType}. If this ends up empty, or {@code codecs} is null, return null. */ - public static @Nullable String getCodecsOfType(String codecs, int trackType) { + public static @Nullable String getCodecsOfType(@Nullable String codecs, int trackType) { String[] codecArray = splitCodecs(codecs); if (codecArray.length == 0) { return null; @@ -1233,9 +1328,9 @@ public final class Util { * Splits a codecs sequence string, as defined in RFC 6381, into individual codec strings. * * @param codecs A codec sequence string, as defined in RFC 6381. - * @return The split codecs, or an array of length zero if the input was empty. + * @return The split codecs, or an array of length zero if the input was empty or null. */ - public static String[] splitCodecs(String codecs) { + public static String[] splitCodecs(@Nullable String codecs) { if (TextUtils.isEmpty(codecs)) { return new String[0]; } @@ -1276,19 +1371,22 @@ public final class Util { public static boolean isEncodingLinearPcm(@C.Encoding int encoding) { return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_16BIT_BIG_ENDIAN || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT || encoding == C.ENCODING_PCM_FLOAT; } /** - * Returns whether {@code encoding} is high resolution (> 16-bit) integer PCM. + * Returns whether {@code encoding} is high resolution (> 16-bit) PCM. * * @param encoding The encoding of the audio data. - * @return Whether the encoding is high resolution integer PCM. + * @return Whether the encoding is high resolution PCM. */ - public static boolean isEncodingHighResolutionIntegerPcm(@C.PcmEncoding int encoding) { - return encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT; + public static boolean isEncodingHighResolutionPcm(@C.PcmEncoding int encoding) { + return encoding == C.ENCODING_PCM_24BIT + || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; } /** @@ -1344,14 +1442,13 @@ public final class Util { case C.ENCODING_PCM_8BIT: return channelCount; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_16BIT_BIG_ENDIAN: return channelCount * 2; case C.ENCODING_PCM_24BIT: return channelCount * 3; case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_FLOAT: return channelCount * 4; - case C.ENCODING_PCM_A_LAW: - case C.ENCODING_PCM_MU_LAW: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -1471,7 +1568,7 @@ public final class Util { * @return The content type. */ @C.ContentType - public static int inferContentType(Uri uri, String overrideExtension) { + public static int inferContentType(Uri uri, @Nullable String overrideExtension) { return TextUtils.isEmpty(overrideExtension) ? inferContentType(uri) : inferContentType("." + overrideExtension); @@ -1667,8 +1764,8 @@ public final class Util { } /** - * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" - * order. + * Returns the result of updating a CRC-32 with the specified bytes in a "most significant bit + * first" order. * * @param bytes Array containing the bytes to update the crc value with. * @param start The index to the first byte in the byte range to update the crc with. @@ -1676,7 +1773,7 @@ public final class Util { * @param initialValue The initial value for the crc calculation. * @return The result of updating the initial value with the specified bytes. */ - public static int crc(byte[] bytes, int start, int end, int initialValue) { + public static int crc32(byte[] bytes, int start, int end, int initialValue) { for (int i = start; i < end; i++) { initialValue = (initialValue << 8) ^ CRC32_BYTES_MSBF[((initialValue >>> 24) ^ (bytes[i] & 0xFF)) & 0xFF]; @@ -1684,6 +1781,23 @@ public final class Util { return initialValue; } + /** + * Returns the result of updating a CRC-8 with the specified bytes in a "most significant bit + * first" order. + * + * @param bytes Array containing the bytes to update the crc value with. + * @param start The index to the first byte in the byte range to update the crc with. + * @param end The index after the last byte in the byte range to update the crc with. + * @param initialValue The initial value for the crc calculation. + * @return The result of updating the initial value with the specified bytes. + */ + public static int crc8(byte[] bytes, int start, int end, int initialValue) { + for (int i = start; i < end; i++) { + initialValue = CRC8_BYTES_MSBF[initialValue ^ (bytes[i] & 0xFF)]; + } + return initialValue; + } + /** * Returns the {@link C.NetworkType} of the current network connection. * @@ -1822,25 +1936,37 @@ public final class Util { } /** - * Gets the physical size of the default display, in pixels. + * Gets the size of the current mode of the default display, in pixels. + * + *

Note that due to application UI scaling, the number of pixels made available to applications + * (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as + * reported by this function). For example, applications running on a display configured with a 4K + * mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take + * advantage of the full mode resolution through a {@link SurfaceView} using full size buffers. * * @param context Any context. - * @return The physical display size, in pixels. + * @return The size of the current mode, in pixels. */ - public static Point getPhysicalDisplaySize(Context context) { + public static Point getCurrentDisplayModeSize(Context context) { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return getPhysicalDisplaySize(context, windowManager.getDefaultDisplay()); + return getCurrentDisplayModeSize(context, windowManager.getDefaultDisplay()); } /** - * Gets the physical size of the specified display, in pixels. + * Gets the size of the current mode of the specified display, in pixels. + * + *

Note that due to application UI scaling, the number of pixels made available to applications + * (as reported by {@link Display#getSize(Point)} may differ from the mode's actual resolution (as + * reported by this function). For example, applications running on a display configured with a 4K + * mode may have their UI laid out and rendered in 1080p and then scaled up. Applications can take + * advantage of the full mode resolution through a {@link SurfaceView} using full size buffers. * * @param context Any context. * @param display The display whose size is to be returned. - * @return The physical display size, in pixels. + * @return The size of the current mode, in pixels. */ - public static Point getPhysicalDisplaySize(Context context, Display display) { - if (Util.SDK_INT <= 28 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { + public static Point getCurrentDisplayModeSize(Context context, Display display) { + if (Util.SDK_INT <= 29 && display.getDisplayId() == Display.DEFAULT_DISPLAY && isTv(context)) { // On Android TVs it is common for the UI to be configured for a lower resolution than // SurfaceViews can output. Before API 26 the Display object does not provide a way to // identify this case, and up to and including API 28 many devices still do not correctly set @@ -1892,13 +2018,10 @@ public final class Util { * Extract renderer capabilities for the renderers created by the provided renderers factory. * * @param renderersFactory A {@link RenderersFactory}. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers. * @return The {@link RendererCapabilities} for each renderer created by the {@code * renderersFactory}. */ - public static RendererCapabilities[] getRendererCapabilities( - RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager) { + public static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) { Renderer[] renderers = renderersFactory.createRenderers( new Handler(), @@ -1906,7 +2029,7 @@ public final class Util { new AudioRendererEventListener() {}, (cues) -> {}, (metadata) -> {}, - drmSessionManager); + /* drmSessionManager= */ null); RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { capabilities[i] = renderers[i].getCapabilities(); @@ -1914,6 +2037,33 @@ public final class Util { return capabilities; } + /** + * Returns a string representation of a {@code TRACK_TYPE_*} constant defined in {@link C}. + * + * @param trackType A {@code TRACK_TYPE_*} constant, + * @return A string representation of this constant. + */ + public static String getTrackTypeString(int trackType) { + switch (trackType) { + case C.TRACK_TYPE_AUDIO: + return "audio"; + case C.TRACK_TYPE_DEFAULT: + return "default"; + case C.TRACK_TYPE_METADATA: + return "metadata"; + case C.TRACK_TYPE_CAMERA_MOTION: + return "camera motion"; + case C.TRACK_TYPE_NONE: + return "none"; + case C.TRACK_TYPE_TEXT: + return "text"; + case C.TRACK_TYPE_VIDEO: + return "video"; + default: + return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?"; + } + } + @Nullable private static String getSystemProperty(String name) { try { @@ -1947,7 +2097,7 @@ public final class Util { Configuration config = Resources.getSystem().getConfiguration(); return SDK_INT >= 24 ? getSystemLocalesV24(config) - : SDK_INT >= 21 ? getSystemLocaleV21(config) : new String[] {config.locale.toString()}; + : new String[] {getLocaleLanguageTag(config.locale)}; } @TargetApi(24) @@ -1956,13 +2106,8 @@ public final class Util { } @TargetApi(21) - private static String[] getSystemLocaleV21(Configuration config) { - return new String[] {config.locale.toLanguageTag()}; - } - - @TargetApi(21) - private static String normalizeLanguageCodeSyntaxV21(String languageTag) { - return Locale.forLanguageTag(languageTag).toLanguageTag(); + private static String getLocaleLanguageTagV21(Locale locale) { + return locale.toLanguageTag(); } private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { @@ -1986,6 +2131,8 @@ public final class Util { return C.NETWORK_TYPE_3G; case TelephonyManager.NETWORK_TYPE_LTE: return C.NETWORK_TYPE_4G; + case TelephonyManager.NETWORK_TYPE_NR: + return C.NETWORK_TYPE_5G; case TelephonyManager.NETWORK_TYPE_IWLAN: return C.NETWORK_TYPE_WIFI; case TelephonyManager.NETWORK_TYPE_GSM: @@ -1995,32 +2142,45 @@ public final class Util { } } - private static HashMap createIso3ToIso2Map() { + private static HashMap createIsoLanguageReplacementMap() { String[] iso2Languages = Locale.getISOLanguages(); - HashMap iso3ToIso2 = + HashMap replacedLanguages = new HashMap<>( - /* initialCapacity= */ iso2Languages.length + iso3BibliographicalToIso2.length); + /* initialCapacity= */ iso2Languages.length + additionalIsoLanguageReplacements.length); for (String iso2 : iso2Languages) { try { // This returns the ISO 639-2/T code for the language. String iso3 = new Locale(iso2).getISO3Language(); if (!TextUtils.isEmpty(iso3)) { - iso3ToIso2.put(iso3, iso2); + replacedLanguages.put(iso3, iso2); } } catch (MissingResourceException e) { // Shouldn't happen for list of known languages, but we don't want to throw either. } } - // Add additional ISO 639-2/B codes to mapping. - for (int i = 0; i < iso3BibliographicalToIso2.length; i += 2) { - iso3ToIso2.put(iso3BibliographicalToIso2[i], iso3BibliographicalToIso2[i + 1]); + // Add additional replacement mappings. + for (int i = 0; i < additionalIsoLanguageReplacements.length; i += 2) { + replacedLanguages.put( + additionalIsoLanguageReplacements[i], additionalIsoLanguageReplacements[i + 1]); } - return iso3ToIso2; + return replacedLanguages; } - // See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. - private static final String[] iso3BibliographicalToIso2 = + private static String maybeReplaceGrandfatheredLanguageTags(String languageTag) { + for (int i = 0; i < isoGrandfatheredTagReplacements.length; i += 2) { + if (languageTag.startsWith(isoGrandfatheredTagReplacements[i])) { + return isoGrandfatheredTagReplacements[i + 1] + + languageTag.substring(/* beginIndex= */ isoGrandfatheredTagReplacements[i].length()); + } + } + return languageTag; + } + + // Additional mapping from ISO3 to ISO2 language codes. + private static final String[] additionalIsoLanguageReplacements = new String[] { + // Bibliographical codes defined in ISO 639-2/B, replaced by terminological code defined in + // ISO 639-2/T. See https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes. "alb", "sq", "arm", "hy", "baq", "eu", @@ -2039,52 +2199,118 @@ public final class Util { "may", "ms", "per", "fa", "rum", "ro", + "scc", "hbs-srp", "slo", "sk", - "wel", "cy" + "wel", "cy", + // Deprecated 2-letter codes, replaced by modern equivalent (including macrolanguage) + // See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, "ISO 639:1988" + "id", "ms-ind", + "iw", "he", + "heb", "he", + "ji", "yi", + // Individual macrolanguage codes mapped back to full macrolanguage code. + // See https://en.wikipedia.org/wiki/ISO_639_macrolanguage + "in", "ms-ind", + "ind", "ms-ind", + "nb", "no-nob", + "nob", "no-nob", + "nn", "no-nno", + "nno", "no-nno", + "tw", "ak-twi", + "twi", "ak-twi", + "bs", "hbs-bos", + "bos", "hbs-bos", + "hr", "hbs-hrv", + "hrv", "hbs-hrv", + "sr", "hbs-srp", + "srp", "hbs-srp", + "cmn", "zh-cmn", + "hak", "zh-hak", + "nan", "zh-nan", + "hsn", "zh-hsn" + }; + + // "Grandfathered tags", replaced by modern equivalents (including macrolanguage) + // See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry. + private static final String[] isoGrandfatheredTagReplacements = + new String[] { + "i-lux", "lb", + "i-hak", "zh-hak", + "i-navajo", "nv", + "no-bok", "no-nob", + "no-nyn", "no-nno", + "zh-guoyu", "zh-cmn", + "zh-hakka", "zh-hak", + "zh-min-nan", "zh-nan", + "zh-xiang", "zh-hsn" }; /** - * Allows the CRC calculation to be done byte by byte instead of bit per bit being the order - * "most significant bit first". + * Allows the CRC-32 calculation to be done byte by byte instead of bit per bit in the order "most + * significant bit first". */ private static final int[] CRC32_BYTES_MSBF = { - 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, - 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, - 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, - 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, - 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, - 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, - 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, - 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, - 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, - 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, - 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, - 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, - 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, - 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, - 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, - 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, - 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, - 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, - 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, - 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, - 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, - 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, - 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, - 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, - 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, - 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, - 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, - 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, - 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, - 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, - 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, - 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, - 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, - 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, - 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, - 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, - 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 + 0X00000000, 0X04C11DB7, 0X09823B6E, 0X0D4326D9, 0X130476DC, 0X17C56B6B, 0X1A864DB2, + 0X1E475005, 0X2608EDB8, 0X22C9F00F, 0X2F8AD6D6, 0X2B4BCB61, 0X350C9B64, 0X31CD86D3, + 0X3C8EA00A, 0X384FBDBD, 0X4C11DB70, 0X48D0C6C7, 0X4593E01E, 0X4152FDA9, 0X5F15ADAC, + 0X5BD4B01B, 0X569796C2, 0X52568B75, 0X6A1936C8, 0X6ED82B7F, 0X639B0DA6, 0X675A1011, + 0X791D4014, 0X7DDC5DA3, 0X709F7B7A, 0X745E66CD, 0X9823B6E0, 0X9CE2AB57, 0X91A18D8E, + 0X95609039, 0X8B27C03C, 0X8FE6DD8B, 0X82A5FB52, 0X8664E6E5, 0XBE2B5B58, 0XBAEA46EF, + 0XB7A96036, 0XB3687D81, 0XAD2F2D84, 0XA9EE3033, 0XA4AD16EA, 0XA06C0B5D, 0XD4326D90, + 0XD0F37027, 0XDDB056FE, 0XD9714B49, 0XC7361B4C, 0XC3F706FB, 0XCEB42022, 0XCA753D95, + 0XF23A8028, 0XF6FB9D9F, 0XFBB8BB46, 0XFF79A6F1, 0XE13EF6F4, 0XE5FFEB43, 0XE8BCCD9A, + 0XEC7DD02D, 0X34867077, 0X30476DC0, 0X3D044B19, 0X39C556AE, 0X278206AB, 0X23431B1C, + 0X2E003DC5, 0X2AC12072, 0X128E9DCF, 0X164F8078, 0X1B0CA6A1, 0X1FCDBB16, 0X018AEB13, + 0X054BF6A4, 0X0808D07D, 0X0CC9CDCA, 0X7897AB07, 0X7C56B6B0, 0X71159069, 0X75D48DDE, + 0X6B93DDDB, 0X6F52C06C, 0X6211E6B5, 0X66D0FB02, 0X5E9F46BF, 0X5A5E5B08, 0X571D7DD1, + 0X53DC6066, 0X4D9B3063, 0X495A2DD4, 0X44190B0D, 0X40D816BA, 0XACA5C697, 0XA864DB20, + 0XA527FDF9, 0XA1E6E04E, 0XBFA1B04B, 0XBB60ADFC, 0XB6238B25, 0XB2E29692, 0X8AAD2B2F, + 0X8E6C3698, 0X832F1041, 0X87EE0DF6, 0X99A95DF3, 0X9D684044, 0X902B669D, 0X94EA7B2A, + 0XE0B41DE7, 0XE4750050, 0XE9362689, 0XEDF73B3E, 0XF3B06B3B, 0XF771768C, 0XFA325055, + 0XFEF34DE2, 0XC6BCF05F, 0XC27DEDE8, 0XCF3ECB31, 0XCBFFD686, 0XD5B88683, 0XD1799B34, + 0XDC3ABDED, 0XD8FBA05A, 0X690CE0EE, 0X6DCDFD59, 0X608EDB80, 0X644FC637, 0X7A089632, + 0X7EC98B85, 0X738AAD5C, 0X774BB0EB, 0X4F040D56, 0X4BC510E1, 0X46863638, 0X42472B8F, + 0X5C007B8A, 0X58C1663D, 0X558240E4, 0X51435D53, 0X251D3B9E, 0X21DC2629, 0X2C9F00F0, + 0X285E1D47, 0X36194D42, 0X32D850F5, 0X3F9B762C, 0X3B5A6B9B, 0X0315D626, 0X07D4CB91, + 0X0A97ED48, 0X0E56F0FF, 0X1011A0FA, 0X14D0BD4D, 0X19939B94, 0X1D528623, 0XF12F560E, + 0XF5EE4BB9, 0XF8AD6D60, 0XFC6C70D7, 0XE22B20D2, 0XE6EA3D65, 0XEBA91BBC, 0XEF68060B, + 0XD727BBB6, 0XD3E6A601, 0XDEA580D8, 0XDA649D6F, 0XC423CD6A, 0XC0E2D0DD, 0XCDA1F604, + 0XC960EBB3, 0XBD3E8D7E, 0XB9FF90C9, 0XB4BCB610, 0XB07DABA7, 0XAE3AFBA2, 0XAAFBE615, + 0XA7B8C0CC, 0XA379DD7B, 0X9B3660C6, 0X9FF77D71, 0X92B45BA8, 0X9675461F, 0X8832161A, + 0X8CF30BAD, 0X81B02D74, 0X857130C3, 0X5D8A9099, 0X594B8D2E, 0X5408ABF7, 0X50C9B640, + 0X4E8EE645, 0X4A4FFBF2, 0X470CDD2B, 0X43CDC09C, 0X7B827D21, 0X7F436096, 0X7200464F, + 0X76C15BF8, 0X68860BFD, 0X6C47164A, 0X61043093, 0X65C52D24, 0X119B4BE9, 0X155A565E, + 0X18197087, 0X1CD86D30, 0X029F3D35, 0X065E2082, 0X0B1D065B, 0X0FDC1BEC, 0X3793A651, + 0X3352BBE6, 0X3E119D3F, 0X3AD08088, 0X2497D08D, 0X2056CD3A, 0X2D15EBE3, 0X29D4F654, + 0XC5A92679, 0XC1683BCE, 0XCC2B1D17, 0XC8EA00A0, 0XD6AD50A5, 0XD26C4D12, 0XDF2F6BCB, + 0XDBEE767C, 0XE3A1CBC1, 0XE760D676, 0XEA23F0AF, 0XEEE2ED18, 0XF0A5BD1D, 0XF464A0AA, + 0XF9278673, 0XFDE69BC4, 0X89B8FD09, 0X8D79E0BE, 0X803AC667, 0X84FBDBD0, 0X9ABC8BD5, + 0X9E7D9662, 0X933EB0BB, 0X97FFAD0C, 0XAFB010B1, 0XAB710D06, 0XA6322BDF, 0XA2F33668, + 0XBCB4666D, 0XB8757BDA, 0XB5365D03, 0XB1F740B4 }; + /** + * Allows the CRC-8 calculation to be done byte by byte instead of bit per bit in the order "most + * significant bit first". + */ + private static final int[] CRC8_BYTES_MSBF = { + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, + 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, + 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, + 0xC3, 0xCA, 0xCD, 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, + 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, + 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, + 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, + 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, + 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, + 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, + 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75, + 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10, + 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, 0x4E, 0x49, 0x40, + 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39, + 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE, + 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, + 0xF3 + }; } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/package-info.java new file mode 100644 index 000000000..76899fc45 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/util/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.util; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java index 1b3943caf..ed2ca9c03 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/ColorInfo.java @@ -51,7 +51,7 @@ public final class ColorInfo implements Parcelable { public final int colorTransfer; /** HdrStaticInfo as defined in CTA-861.3, or null if none specified. */ - public final @Nullable byte[] hdrStaticInfo; + @Nullable public final byte[] hdrStaticInfo; // Lazily initialized hashcode. private int hashCode; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java index 3aeff9d55..3a13540e1 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/DolbyVisionConfig.java @@ -36,7 +36,7 @@ public final class DolbyVisionConfig { int dvProfile = (profileData >> 1); int dvLevel = ((profileData & 0x1) << 5) | ((data.readUnsignedByte() >> 3) & 0x1F); String codecsPrefix; - if (dvProfile == 4 || dvProfile == 5) { + if (dvProfile == 4 || dvProfile == 5 || dvProfile == 7) { codecsPrefix = "dvhe"; } else if (dvProfile == 8) { codecsPrefix = "hev1"; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index f302279f0..0a900999b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -21,35 +21,27 @@ import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_S import android.annotation.TargetApi; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; -import android.opengl.EGL14; -import android.opengl.EGLDisplay; import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; -import androidx.annotation.Nullable; import android.view.Surface; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.EGLSurfaceTexture; import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode; +import com.google.android.exoplayer2.util.GlUtil; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; -import javax.microedition.khronos.egl.EGL10; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** - * A dummy {@link Surface}. - */ +/** A dummy {@link Surface}. */ @TargetApi(17) public final class DummySurface extends Surface { private static final String TAG = "DummySurface"; - private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; - private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; - /** * Whether the surface is secure. */ @@ -69,7 +61,7 @@ public final class DummySurface extends Surface { */ public static synchronized boolean isSecureSupported(Context context) { if (!secureModeInitialized) { - secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context); + secureMode = getSecureMode(context); secureModeInitialized = true; } return secureMode != SECURE_MODE_NONE; @@ -121,34 +113,21 @@ public final class DummySurface extends Surface { } } - @TargetApi(24) - private static @SecureMode int getSecureModeV24(Context context) { - if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { - // Samsung devices running Nougat are known to be broken. See - // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. - // Moto Z XT1650 is also affected. See - // https://github.com/google/ExoPlayer/issues/3215. + @SecureMode + private static int getSecureMode(Context context) { + if (GlUtil.isProtectedContentExtensionSupported(context)) { + if (GlUtil.isSurfacelessContextExtensionSupported()) { + return SECURE_MODE_SURFACELESS_CONTEXT; + } else { + // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. + // This may require support for EXT_protected_surface, but in practice it works on some + // devices that don't have that extension. See also + // https://github.com/google/ExoPlayer/issues/3558. + return SECURE_MODE_PROTECTED_PBUFFER; + } + } else { return SECURE_MODE_NONE; } - if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { - // Pre API level 26 devices were not well tested unless they supported VR mode. - return SECURE_MODE_NONE; - } - EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); - String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - if (eglExtensions == null) { - return SECURE_MODE_NONE; - } - if (!eglExtensions.contains(EXTENSION_PROTECTED_CONTENT)) { - return SECURE_MODE_NONE; - } - // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. This may - // require support for EXT_protected_surface, but in practice it works on some devices that - // don't have that extension. See also https://github.com/google/ExoPlayer/issues/3558. - return eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT) - ? SECURE_MODE_SURFACELESS_CONTEXT - : SECURE_MODE_PROTECTED_PBUFFER; } private static class DummySurfaceThread extends HandlerThread implements Callback { @@ -158,9 +137,9 @@ public final class DummySurface extends Surface { private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture; private @MonotonicNonNull Handler handler; - private @Nullable Error initError; - private @Nullable RuntimeException initException; - private @Nullable DummySurface surface; + @Nullable private Error initError; + @Nullable private RuntimeException initException; + @Nullable private DummySurface surface; public DummySurfaceThread() { super("dummySurface"); diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java index 727883f67..bb11ef000 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java @@ -27,7 +27,7 @@ import java.util.List; */ public final class HevcConfig { - public final @Nullable List initializationData; + @Nullable public final List initializationData; public final int nalUnitLengthFieldLength; /** diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 505e46cc4..0d36c0280 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -21,20 +21,24 @@ import android.content.Context; import android.graphics.Point; import android.media.MediaCodec; import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCrypto; import android.media.MediaFormat; +import android.os.Bundle; import android.os.Handler; +import android.os.Message; import android.os.SystemClock; -import androidx.annotation.CallSuper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.Pair; import android.view.Surface; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.PlayerMessage.Target; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -45,6 +49,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaFormatUtil; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; @@ -91,6 +96,26 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { */ private static final float INITIAL_FORMAT_MAX_INPUT_SIZE_SCALE_FACTOR = 1.5f; + /** Magic frame render timestamp that indicates the EOS in tunneling mode. */ + private static final long TUNNELING_EOS_PRESENTATION_TIME_US = Long.MAX_VALUE; + + /** A {@link DecoderException} with additional surface information. */ + public static final class VideoDecoderException extends DecoderException { + + /** The {@link System#identityHashCode(Object)} of the surface when the exception occurred. */ + public final int surfaceIdentityHashCode; + + /** Whether the surface was valid when the exception occurred. */ + public final boolean isSurfaceValid; + + public VideoDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo, @Nullable Surface surface) { + super(cause, codecInfo); + surfaceIdentityHashCode = System.identityHashCode(surface); + isSurfaceValid = surface == null || surface.isValid(); + } + } + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; private static boolean deviceNeedsSetOutputSurfaceWorkaround; @@ -105,6 +130,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private CodecMaxValues codecMaxValues; private boolean codecNeedsSetOutputSurfaceWorkaround; + private boolean codecHandlesHdr10PlusOutOfBandMetadata; private Surface surface; private Surface dummySurface; @@ -121,6 +147,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private int pendingRotationDegrees; private float pendingPixelWidthHeightRatio; + @Nullable private MediaFormat currentMediaFormat; private int currentWidth; private int currentHeight; private int currentUnappliedRotationDegrees; @@ -132,12 +159,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean tunneling; private int tunnelingAudioSessionId; - /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; + /* package */ @Nullable OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; private long lastInputTimeUs; private long outputStreamOffsetUs; private int pendingOutputStreamOffsetCount; - private @Nullable VideoFrameMetadataListener frameMetadataListener; + @Nullable private VideoFrameMetadataListener frameMetadataListener; /** * @param context A context. @@ -175,6 +202,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ + @SuppressWarnings("deprecation") public MediaCodecVideoRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -210,7 +238,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, + * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated + @SuppressWarnings("deprecation") public MediaCodecVideoRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -232,6 +265,41 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maxDroppedFramesToNotify); } + /** + * @param context A context. + * @param mediaCodecSelector A decoder selector. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param enableDecoderFallback Whether to enable fallback to lower-priority decoders if decoder + * initialization fails. This may result in using a decoder that is slower/less efficient than + * the primary decoder. + * @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + */ + @SuppressWarnings("deprecation") + public MediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + boolean enableDecoderFallback, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify) { + this( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + /* drmSessionManager= */ null, + /* playClearSamplesWithoutKeys= */ false, + enableDecoderFallback, + eventHandler, + eventListener, + maxDroppedFramesToNotify); + } + /** * @param context A context. * @param mediaCodecSelector A decoder selector. @@ -252,7 +320,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param eventListener A listener of events. May be null if delivery of events is not required. * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @deprecated Use {@link #MediaCodecVideoRenderer(Context, MediaCodecSelector, long, boolean, + * Handler, VideoRendererEventListener, int)} instead, and pass DRM-related parameters to the + * {@link MediaSource} factories. */ + @Deprecated public MediaCodecVideoRenderer( Context context, MediaCodecSelector mediaCodecSelector, @@ -290,48 +362,59 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, Format format) + @Capabilities + protected int supportsFormat( + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isVideo(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; - } - boolean requiresSecureDecryption = false; - DrmInitData drmInitData = format.drmInitData; - if (drmInitData != null) { - for (int i = 0; i < drmInitData.schemeDataCount; i++) { - requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; - } + return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } + @Nullable DrmInitData drmInitData = format.drmInitData; + // Assume encrypted content requires secure decoders. + boolean requiresSecureDecryption = drmInitData != null; List decoderInfos = - getDecoderInfos(mediaCodecSelector, format, requiresSecureDecryption); - if (decoderInfos.isEmpty()) { - return requiresSecureDecryption - && !mediaCodecSelector - .getDecoderInfos( - format.sampleMimeType, - /* requiresSecureDecoder= */ false, - /* requiresTunnelingDecoder= */ false) - .isEmpty() - ? FORMAT_UNSUPPORTED_DRM - : FORMAT_UNSUPPORTED_SUBTYPE; + getDecoderInfos( + mediaCodecSelector, + format, + requiresSecureDecryption, + /* requiresTunnelingDecoder= */ false); + if (requiresSecureDecryption && decoderInfos.isEmpty()) { + // No secure decoders are available. Fall back to non-secure decoders. + decoderInfos = + getDecoderInfos( + mediaCodecSelector, + format, + /* requiresSecureDecoder= */ false, + /* requiresTunnelingDecoder= */ false); } - if (!supportsFormatDrm(drmSessionManager, drmInitData)) { - return FORMAT_UNSUPPORTED_DRM; + if (decoderInfos.isEmpty()) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); + } + boolean supportsFormatDrm = + drmInitData == null + || FrameworkMediaCrypto.class.equals(format.exoMediaCryptoType) + || (format.exoMediaCryptoType == null + && supportsFormatDrm(drmSessionManager, drmInitData)); + if (!supportsFormatDrm) { + return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); } // Check capabilities for the first decoder in the list, which takes priority. MediaCodecInfo decoderInfo = decoderInfos.get(0); boolean isFormatSupported = decoderInfo.isFormatSupported(format); + @AdaptiveSupport int adaptiveSupport = decoderInfo.isSeamlessAdaptationSupported(format) ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS; - int tunnelingSupport = TUNNELING_NOT_SUPPORTED; + @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; if (isFormatSupported) { List tunnelingDecoderInfos = - mediaCodecSelector.getDecoderInfos( - format.sampleMimeType, + getDecoderInfos( + mediaCodecSelector, + format, requiresSecureDecryption, /* requiresTunnelingDecoder= */ true); if (!tunnelingDecoderInfos.isEmpty()) { @@ -342,16 +425,50 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } } + @FormatSupport int formatSupport = isFormatSupported ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; - return adaptiveSupport | tunnelingSupport | formatSupport; + return RendererCapabilities.create(formatSupport, adaptiveSupport, tunnelingSupport); } @Override protected List getDecoderInfos( MediaCodecSelector mediaCodecSelector, Format format, boolean requiresSecureDecoder) throws DecoderQueryException { + return getDecoderInfos(mediaCodecSelector, format, requiresSecureDecoder, tunneling); + } + + private static List getDecoderInfos( + MediaCodecSelector mediaCodecSelector, + Format format, + boolean requiresSecureDecoder, + boolean requiresTunnelingDecoder) + throws DecoderQueryException { + @Nullable String mimeType = format.sampleMimeType; + if (mimeType == null) { + return Collections.emptyList(); + } List decoderInfos = - mediaCodecSelector.getDecoderInfos(format.sampleMimeType, requiresSecureDecoder, tunneling); + mediaCodecSelector.getDecoderInfos( + mimeType, requiresSecureDecoder, requiresTunnelingDecoder); + decoderInfos = MediaCodecUtil.getDecoderInfosSortedByFormatSupport(decoderInfos, format); + if (MimeTypes.VIDEO_DOLBY_VISION.equals(mimeType)) { + // Fall back to H.264/AVC or H.265/HEVC for the relevant DV profiles. + @Nullable + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); + if (codecProfileAndLevel != null) { + int profile = codecProfileAndLevel.first; + if (profile == CodecProfileLevel.DolbyVisionProfileDvheDtr + || profile == CodecProfileLevel.DolbyVisionProfileDvheSt) { + decoderInfos.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.VIDEO_H265, requiresSecureDecoder, requiresTunnelingDecoder)); + } else if (profile == CodecProfileLevel.DolbyVisionProfileDvavSe) { + decoderInfos.addAll( + mediaCodecSelector.getDecoderInfos( + MimeTypes.VIDEO_H264, requiresSecureDecoder, requiresTunnelingDecoder)); + } + } + } return Collections.unmodifiableList(decoderInfos); } @@ -443,6 +560,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { lastInputTimeUs = C.TIME_UNSET; outputStreamOffsetUs = C.TIME_UNSET; pendingOutputStreamOffsetCount = 0; + currentMediaFormat = null; clearReportedVideoSize(); clearRenderedFirstFrame(); frameReleaseTimeHelper.disable(); @@ -544,8 +662,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected boolean getCodecNeedsEosPropagation() { - // In tunneling mode we can't dequeue an end-of-stream buffer, so propagate it in the renderer. - return tunneling; + // Since API 23, onFrameRenderedListener allows for detection of the renderer EOS. + return tunneling && Util.SDK_INT < 23; } @Override @@ -553,7 +671,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaCodecInfo codecInfo, MediaCodec codec, Format format, - MediaCrypto crypto, + @Nullable MediaCrypto crypto, float codecOperatingRate) { String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); @@ -633,11 +751,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { long initializationDurationMs) { eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); + codecHandlesHdr10PlusOutOfBandMetadata = + Assertions.checkNotNull(getCodecInfo()).isHdr10PlusOutOfBandMetadataSupported(); } @Override - protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { - super.onInputFormatChanged(newFormat); + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + super.onInputFormatChanged(formatHolder); + Format newFormat = formatHolder.format; eventDispatcher.inputFormatChanged(newFormat); pendingPixelWidthHeightRatio = newFormat.pixelWidthHeightRatio; pendingRotationDegrees = newFormat.rotationDegrees; @@ -651,7 +772,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @CallSuper @Override protected void onQueueInputBuffer(DecoderInputBuffer buffer) { - buffersInCodecCount++; + // In tunneling mode the device may do frame rate conversion, so in general we can't keep track + // of the number of buffers in the codec. + if (!tunneling) { + buffersInCodecCount++; + } lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); if (Util.SDK_INT < 23 && tunneling) { // In tunneled mode before API 23 we don't have a way to know when the buffer is output, so @@ -661,21 +786,59 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { - boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) - && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM) - && outputFormat.containsKey(KEY_CROP_TOP); + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputMediaFormat) { + currentMediaFormat = outputMediaFormat; + boolean hasCrop = + outputMediaFormat.containsKey(KEY_CROP_RIGHT) + && outputMediaFormat.containsKey(KEY_CROP_LEFT) + && outputMediaFormat.containsKey(KEY_CROP_BOTTOM) + && outputMediaFormat.containsKey(KEY_CROP_TOP); int width = hasCrop - ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1 - : outputFormat.getInteger(MediaFormat.KEY_WIDTH); + ? outputMediaFormat.getInteger(KEY_CROP_RIGHT) + - outputMediaFormat.getInteger(KEY_CROP_LEFT) + + 1 + : outputMediaFormat.getInteger(MediaFormat.KEY_WIDTH); int height = hasCrop - ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1 - : outputFormat.getInteger(MediaFormat.KEY_HEIGHT); + ? outputMediaFormat.getInteger(KEY_CROP_BOTTOM) + - outputMediaFormat.getInteger(KEY_CROP_TOP) + + 1 + : outputMediaFormat.getInteger(MediaFormat.KEY_HEIGHT); processOutputFormat(codec, width, height); } + @Override + protected void handleInputBufferSupplementalData(DecoderInputBuffer buffer) + throws ExoPlaybackException { + if (!codecHandlesHdr10PlusOutOfBandMetadata) { + return; + } + ByteBuffer data = Assertions.checkNotNull(buffer.supplementalData); + if (data.remaining() >= 7) { + // Check for HDR10+ out-of-band metadata. See User_data_registered_itu_t_t35 in ST 2094-40. + byte ituTT35CountryCode = data.get(); + int ituTT35TerminalProviderCode = data.getShort(); + int ituTT35TerminalProviderOrientedCode = data.getShort(); + byte applicationIdentifier = data.get(); + byte applicationVersion = data.get(); + data.position(0); + if (ituTT35CountryCode == (byte) 0xB5 + && ituTT35TerminalProviderCode == 0x003C + && ituTT35TerminalProviderOrientedCode == 0x0001 + && applicationIdentifier == 4 + && applicationVersion == 0) { + // The metadata size may vary so allocate a new array every time. This is not too + // inefficient because the metadata is only a few tens of bytes. + byte[] hdr10PlusInfo = new byte[data.remaining()]; + data.get(hdr10PlusInfo); + data.position(0); + // If codecHandlesHdr10PlusOutOfBandMetadata is true, this is an API 29 or later build. + setHdr10PlusInfoV29(getCodec(), hdr10PlusInfo); + } + } + } + @Override protected boolean processOutputBuffer( long positionUs, @@ -711,12 +874,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; + long elapsedSinceLastRenderUs = elapsedRealtimeNowUs - lastRenderTimeUs; boolean isStarted = getState() == STATE_STARTED; - if (!renderedFirstFrame - || (isStarted - && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) { + // Don't force output until we joined and the position reached the current stream. + boolean forceRenderOutputBuffer = + joiningDeadlineMs == C.TIME_UNSET + && positionUs >= outputStreamOffsetUs + && (!renderedFirstFrame + || (isStarted && shouldForceRenderOutputBuffer(earlyUs, elapsedSinceLastRenderUs))); + if (forceRenderOutputBuffer) { long releaseTimeNs = System.nanoTime(); - notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format); + notifyFrameMetadataListener(presentationTimeUs, releaseTimeNs, format, currentMediaFormat); if (Util.SDK_INT >= 21) { renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, releaseTimeNs); } else { @@ -743,18 +911,25 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; + boolean treatDroppedBuffersAsSkipped = joiningDeadlineMs != C.TIME_UNSET; if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs, isLastBuffer) - && maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) { + && maybeDropBuffersToKeyframe( + codec, bufferIndex, presentationTimeUs, positionUs, treatDroppedBuffersAsSkipped)) { return false; } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs, isLastBuffer)) { - dropOutputBuffer(codec, bufferIndex, presentationTimeUs); + if (treatDroppedBuffersAsSkipped) { + skipOutputBuffer(codec, bufferIndex, presentationTimeUs); + } else { + dropOutputBuffer(codec, bufferIndex, presentationTimeUs); + } return true; } if (Util.SDK_INT >= 21) { // Let the underlying framework time the release. if (earlyUs < 50000) { - notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format); + notifyFrameMetadataListener( + presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat); renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs); return true; } @@ -772,7 +947,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } } - notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format); + notifyFrameMetadataListener( + presentationTimeUs, adjustedReleaseTimeNs, format, currentMediaFormat); renderOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -800,15 +976,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // On API level 20 and below the decoder does not apply the rotation. currentUnappliedRotationDegrees = pendingRotationDegrees; } - // Must be applied each time the output format changes. + // Must be applied each time the output MediaFormat changes. codec.setVideoScalingMode(scalingMode); } private void notifyFrameMetadataListener( - long presentationTimeUs, long releaseTimeNs, Format format) { + long presentationTimeUs, long releaseTimeNs, Format format, MediaFormat mediaFormat) { if (frameMetadataListener != null) { frameMetadataListener.onVideoFrameAboutToBeRendered( - presentationTimeUs, releaseTimeNs, format); + presentationTimeUs, releaseTimeNs, format, mediaFormat); } } @@ -828,10 +1004,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { processOutputFormat(getCodec(), format.width, format.height); } maybeNotifyVideoSizeChanged(); + decoderCounters.renderedOutputBufferCount++; maybeNotifyRenderedFirstFrame(); onProcessedOutputBuffer(presentationTimeUs); } + /** Called when a output EOS was received in tunneling mode. */ + private void onProcessedTunneledEndOfStream() { + setPendingOutputEndOfStream(); + } + /** * Called when an output buffer is successfully processed. * @@ -840,7 +1022,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @CallSuper @Override protected void onProcessedOutputBuffer(long presentationTimeUs) { - buffersInCodecCount--; + if (!tunneling) { + buffersInCodecCount--; + } while (pendingOutputStreamOffsetCount != 0 && presentationTimeUs >= pendingOutputStreamSwitchTimesUs[0]) { outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; @@ -857,6 +1041,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { pendingOutputStreamSwitchTimesUs, /* destPos= */ 0, pendingOutputStreamOffsetCount); + clearRenderedFirstFrame(); } } @@ -899,6 +1084,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @return Returns whether to force rendering an output buffer. */ protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) { + // Force render late buffers every 100ms to avoid frozen video effect. return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; } @@ -939,11 +1125,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param index The index of the output buffer to drop. * @param presentationTimeUs The presentation time of the output buffer, in microseconds. * @param positionUs The current playback position, in microseconds. + * @param treatDroppedBuffersAsSkipped Whether dropped buffers should be treated as intentionally + * skipped. * @return Whether any buffers were dropped. * @throws ExoPlaybackException If an error occurs flushing the codec. */ - protected boolean maybeDropBuffersToKeyframe(MediaCodec codec, int index, long presentationTimeUs, - long positionUs) throws ExoPlaybackException { + protected boolean maybeDropBuffersToKeyframe( + MediaCodec codec, + int index, + long presentationTimeUs, + long positionUs, + boolean treatDroppedBuffersAsSkipped) + throws ExoPlaybackException { int droppedSourceBufferCount = skipSource(positionUs); if (droppedSourceBufferCount == 0) { return false; @@ -951,7 +1144,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { decoderCounters.droppedToKeyframeCount++; // We dropped some buffers to catch up, so update the decoder counters and flush the codec, // which releases all pending buffers buffers including the current output buffer. - updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); + int totalDroppedBufferCount = buffersInCodecCount + droppedSourceBufferCount; + if (treatDroppedBuffersAsSkipped) { + decoderCounters.skippedOutputBufferCount += totalDroppedBufferCount; + } else { + updateDroppedBufferCounters(totalDroppedBufferCount); + } flushOrReinitializeCodec(); return true; } @@ -1102,6 +1300,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return earlyUs < -500000; } + @TargetApi(29) + private static void setHdr10PlusInfoV29(MediaCodec codec, byte[] hdr10PlusInfo) { + Bundle codecParameters = new Bundle(); + codecParameters.putByteArray(MediaCodec.PARAMETER_KEY_HDR10_PLUS_INFO, hdr10PlusInfo); + codec.setParameters(codecParameters); + } + @TargetApi(23) private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { codec.setOutputSurface(surface); @@ -1116,7 +1321,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { /** * Returns the framework {@link MediaFormat} that should be used to configure the decoder. * - * @param format The format of media. + * @param format The {@link Format} of media. * @param codecMimeType The MIME type handled by the codec. * @param codecMaxValues Codec max values that should be used when configuring the decoder. * @param codecOperatingRate The codec operating rate, or {@link #CODEC_OPERATING_RATE_UNSET} if @@ -1148,8 +1353,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { // Some phones require the profile to be set on the codec. // See https://github.com/google/ExoPlayer/pull/5438. - Pair codecProfileAndLevel = - MediaCodecUtil.getCodecProfileAndLevel(format.codecs); + Pair codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); if (codecProfileAndLevel != null) { MediaFormatUtil.maybeSetInteger( mediaFormat, MediaFormat.KEY_PROFILE, codecProfileAndLevel.first); @@ -1182,7 +1386,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * that will allow possible adaptation to other compatible formats in {@code streamFormats}. * * @param codecInfo Information about the {@link MediaCodec} being configured. - * @param format The format for which the codec is being configured. + * @param format The {@link Format} for which the codec is being configured. * @param streamFormats The possible stream formats. * @return Suitable {@link CodecMaxValues}. */ @@ -1236,13 +1440,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + @Override + protected DecoderException createDecoderException( + Throwable cause, @Nullable MediaCodecInfo codecInfo) { + return new VideoDecoderException(cause, codecInfo, surface); + } + /** * Returns a maximum video size to use when configuring a codec for {@code format} in a way that * will allow possible adaptation to other compatible formats that are expected to have the same * aspect ratio, but whose sizes are unknown. * * @param codecInfo Information about the {@link MediaCodec} being configured. - * @param format The format for which the codec is being configured. + * @param format The {@link Format} for which the codec is being configured. * @return The maximum video size to use, or null if the size of {@code format} should be used. */ private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) { @@ -1282,7 +1492,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum input buffer size for a given codec and format. + * Returns a maximum input buffer size for a given {@link MediaCodec} and {@link Format}. * * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format. @@ -1410,9 +1620,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } synchronized (MediaCodecVideoRenderer.class) { if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { - if (Util.SDK_INT <= 27 && ("dangal".equals(Util.DEVICE) || "HWEML".equals(Util.DEVICE))) { - // A small number of devices are affected on API level 27: - // https://github.com/google/ExoPlayer/issues/5169. + if ("dangal".equals(Util.DEVICE)) { + // Workaround for MiTV devices: + // https://github.com/google/ExoPlayer/issues/5169, + // https://github.com/google/ExoPlayer/issues/6899. + deviceNeedsSetOutputSurfaceWorkaround = true; + } else if (Util.SDK_INT <= 27 && "HWEML".equals(Util.DEVICE)) { + // Workaround for Huawei P20: + // https://github.com/google/ExoPlayer/issues/4468#issuecomment-459291645. deviceNeedsSetOutputSurfaceWorkaround = true; } else if (Util.SDK_INT >= 27) { // In general, devices running API level 27 or later should be unaffected. Do nothing. @@ -1431,7 +1646,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/4419, // https://github.com/google/ExoPlayer/issues/4460, // https://github.com/google/ExoPlayer/issues/4468, - // https://github.com/google/ExoPlayer/issues/5312. + // https://github.com/google/ExoPlayer/issues/5312, + // https://github.com/google/ExoPlayer/issues/6503. switch (Util.DEVICE) { case "1601": case "1713": @@ -1498,6 +1714,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "JGZ": case "K50a40": case "kate": + case "l5460": case "le_x6": case "LS-5017": case "M5c": @@ -1567,6 +1784,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { switch (Util.MODEL) { case "AFTA": case "AFTN": + case "JSN-L21": deviceNeedsSetOutputSurfaceWorkaround = true; break; default: @@ -1580,6 +1798,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return deviceNeedsSetOutputSurfaceWorkaround; } + protected Surface getSurface() { + return surface; + } + protected static final class CodecMaxValues { public final int width; @@ -1595,21 +1817,61 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @TargetApi(23) - private final class OnFrameRenderedListenerV23 implements MediaCodec.OnFrameRenderedListener { + private final class OnFrameRenderedListenerV23 + implements MediaCodec.OnFrameRenderedListener, Handler.Callback { - private OnFrameRenderedListenerV23(MediaCodec codec) { - codec.setOnFrameRenderedListener(this, new Handler()); + private static final int HANDLE_FRAME_RENDERED = 0; + + private final Handler handler; + + public OnFrameRenderedListenerV23(MediaCodec codec) { + handler = new Handler(this); + codec.setOnFrameRenderedListener(/* listener= */ this, handler); } @Override - public void onFrameRendered(@NonNull MediaCodec codec, long presentationTimeUs, long nanoTime) { + public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) { + // Workaround bug in MediaCodec that causes deadlock if you call directly back into the + // MediaCodec from this listener method. + // Deadlock occurs because MediaCodec calls this listener method holding a lock, + // which may also be required by calls made back into the MediaCodec. + // This was fixed in https://android-review.googlesource.com/1156807. + // + // The workaround queues the event for subsequent processing, where the lock will not be held. + if (Util.SDK_INT < 30) { + Message message = + Message.obtain( + handler, + /* what= */ HANDLE_FRAME_RENDERED, + /* arg1= */ (int) (presentationTimeUs >> 32), + /* arg2= */ (int) presentationTimeUs); + handler.sendMessageAtFrontOfQueue(message); + } else { + handleFrameRendered(presentationTimeUs); + } + } + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case HANDLE_FRAME_RENDERED: + handleFrameRendered(Util.toLong(message.arg1, message.arg2)); + return true; + default: + return false; + } + } + + private void handleFrameRendered(long presentationTimeUs) { if (this != tunnelingOnFrameRenderedListener) { // Stale event. return; } - onProcessedTunneledBuffer(presentationTimeUs); + if (presentationTimeUs == TUNNELING_EOS_PRESENTATION_TIME_US) { + onProcessedTunneledEndOfStream(); + } else { + onProcessedTunneledBuffer(presentationTimeUs); + } } - } - } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java new file mode 100644 index 000000000..6d8147b01 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/SimpleDecoderVideoRenderer.java @@ -0,0 +1,975 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.video; + +import android.os.Handler; +import android.os.SystemClock; +import android.view.Surface; +import androidx.annotation.CallSuper; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.BaseRenderer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.SimpleDecoder; +import com.google.android.exoplayer2.drm.DrmSession; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.TimedValueQueue; +import com.google.android.exoplayer2.util.TraceUtil; +import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Decodes and renders video using a {@link SimpleDecoder}. */ +public abstract class SimpleDecoderVideoRenderer extends BaseRenderer { + + /** Decoder reinitialization states. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + REINITIALIZATION_STATE_NONE, + REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, + REINITIALIZATION_STATE_WAIT_END_OF_STREAM + }) + private @interface ReinitializationState {} + /** The decoder 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 decoder to be re-initialized, but we + * haven't yet signaled an end of stream to the existing decoder. 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 decoder to be re-initialized, and we've + * signaled an end of stream to the existing decoder. We're waiting for the decoder 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; + + private final long allowedJoiningTimeMs; + private final int maxDroppedFramesToNotify; + private final boolean playClearSamplesWithoutKeys; + private final EventDispatcher eventDispatcher; + private final TimedValueQueue formatQueue; + private final DecoderInputBuffer flagsOnlyBuffer; + private final DrmSessionManager drmSessionManager; + + private boolean drmResourcesAcquired; + private Format inputFormat; + private Format outputFormat; + private SimpleDecoder< + VideoDecoderInputBuffer, + ? extends VideoDecoderOutputBuffer, + ? extends VideoDecoderException> + decoder; + private VideoDecoderInputBuffer inputBuffer; + private VideoDecoderOutputBuffer outputBuffer; + @Nullable private Surface surface; + @Nullable private VideoDecoderOutputBufferRenderer outputBufferRenderer; + @C.VideoOutputMode private int outputMode; + + @Nullable private DrmSession decoderDrmSession; + @Nullable private DrmSession sourceDrmSession; + + @ReinitializationState private int decoderReinitializationState; + private boolean decoderReceivedBuffers; + + private boolean renderedFirstFrame; + private long initialPositionUs; + private long joiningDeadlineMs; + private boolean waitingForKeys; + private boolean waitingForFirstSampleInFormat; + + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private int reportedWidth; + private int reportedHeight; + + private long droppedFrameAccumulationStartTimeMs; + private int droppedFrames; + private int consecutiveDroppedFrameCount; + private int buffersInCodecCount; + private long lastRenderTimeUs; + private long outputStreamOffsetUs; + + /** Decoder event counters used for debugging purposes. */ + protected DecoderCounters decoderCounters; + + /** + * @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 maxDroppedFramesToNotify The maximum number of frames that can be dropped between + * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. + * @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. + */ + protected SimpleDecoderVideoRenderer( + long allowedJoiningTimeMs, + @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, + int maxDroppedFramesToNotify, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys) { + super(C.TRACK_TYPE_VIDEO); + this.allowedJoiningTimeMs = allowedJoiningTimeMs; + this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; + this.drmSessionManager = drmSessionManager; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + joiningDeadlineMs = C.TIME_UNSET; + clearReportedVideoSize(); + formatQueue = new TimedValueQueue<>(); + flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); + eventDispatcher = new EventDispatcher(eventHandler, eventListener); + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + outputMode = C.VIDEO_OUTPUT_MODE_NONE; + } + + // BaseRenderer implementation. + + @Override + @Capabilities + public final int supportsFormat(Format format) { + return supportsFormatInternal(drmSessionManager, format); + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + if (outputStreamEnded) { + return; + } + + if (inputFormat == null) { + // We don't have a format yet, so try and read one. + FormatHolder formatHolder = getFormatHolder(); + flagsOnlyBuffer.clear(); + int result = readSource(formatHolder, flagsOnlyBuffer, true); + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder); + } else if (result == C.RESULT_BUFFER_READ) { + // End of stream read having not read a format. + Assertions.checkState(flagsOnlyBuffer.isEndOfStream()); + inputStreamEnded = true; + outputStreamEnded = true; + return; + } else { + // We still don't have a format and can't make progress without one. + return; + } + } + + // If we don't have a decoder yet, we need to instantiate one. + maybeInitDecoder(); + + if (decoder != null) { + try { + // Rendering loop. + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + while (feedInputBuffer()) {} + TraceUtil.endSection(); + } catch (VideoDecoderException e) { + throw createRendererException(e, inputFormat); + } + decoderCounters.ensureUpdated(); + } + } + + @Override + public boolean isEnded() { + return outputStreamEnded; + } + + @Override + public boolean isReady() { + if (waitingForKeys) { + return false; + } + if (inputFormat != null + && (isSourceReady() || outputBuffer != null) + && (renderedFirstFrame || !hasOutput())) { + // Ready. If we were joining then we've now joined, so clear the joining deadline. + joiningDeadlineMs = C.TIME_UNSET; + return true; + } else if (joiningDeadlineMs == C.TIME_UNSET) { + // Not joining. + return false; + } else if (SystemClock.elapsedRealtime() < joiningDeadlineMs) { + // Joining and still within the joining deadline. + return true; + } else { + // The joining deadline has been exceeded. Give up and clear the deadline. + joiningDeadlineMs = C.TIME_UNSET; + return false; + } + } + + // Protected methods. + + @Override + protected void onEnabled(boolean joining) throws ExoPlaybackException { + if (drmSessionManager != null && !drmResourcesAcquired) { + drmResourcesAcquired = true; + drmSessionManager.prepare(); + } + decoderCounters = new DecoderCounters(); + eventDispatcher.enabled(decoderCounters); + } + + @Override + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + inputStreamEnded = false; + outputStreamEnded = false; + clearRenderedFirstFrame(); + initialPositionUs = C.TIME_UNSET; + consecutiveDroppedFrameCount = 0; + if (decoder != null) { + flushDecoder(); + } + if (joining) { + setJoiningDeadlineMs(); + } else { + joiningDeadlineMs = C.TIME_UNSET; + } + formatQueue.clear(); + } + + @Override + protected void onStarted() { + droppedFrames = 0; + droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; + } + + @Override + protected void onStopped() { + joiningDeadlineMs = C.TIME_UNSET; + maybeNotifyDroppedFrames(); + } + + @Override + protected void onDisabled() { + inputFormat = null; + waitingForKeys = false; + clearReportedVideoSize(); + clearRenderedFirstFrame(); + try { + setSourceDrmSession(null); + releaseDecoder(); + } finally { + eventDispatcher.disabled(decoderCounters); + } + } + + @Override + protected void onReset() { + if (drmSessionManager != null && drmResourcesAcquired) { + drmResourcesAcquired = false; + drmSessionManager.release(); + } + } + + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + outputStreamOffsetUs = offsetUs; + super.onStreamChanged(formats, offsetUs); + } + + /** + * Called when a decoder has been created and configured. + * + *

The default implementation is a no-op. + * + * @param name The name of the decoder that was initialized. + * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization + * finished. + * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds. + */ + @CallSuper + protected void onDecoderInitialized( + String name, long initializedTimestampMs, long initializationDurationMs) { + eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + } + + /** + * Flushes the decoder. + * + * @throws ExoPlaybackException If an error occurs reinitializing a decoder. + */ + @CallSuper + protected void flushDecoder() throws ExoPlaybackException { + waitingForKeys = false; + buffersInCodecCount = 0; + if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { + releaseDecoder(); + maybeInitDecoder(); + } else { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; + } + decoder.flush(); + decoderReceivedBuffers = false; + } + } + + /** Releases the decoder. */ + @CallSuper + protected void releaseDecoder() { + inputBuffer = null; + outputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_NONE; + decoderReceivedBuffers = false; + buffersInCodecCount = 0; + if (decoder != null) { + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + } + setDecoderDrmSession(null); + } + + /** + * Called when a new format is read from the upstream source. + * + * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. + * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. + */ + @CallSuper + @SuppressWarnings("unchecked") + protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + waitingForFirstSampleInFormat = true; + Format newFormat = Assertions.checkNotNull(formatHolder.format); + if (formatHolder.includesDrmSession) { + setSourceDrmSession((DrmSession) formatHolder.drmSession); + } else { + sourceDrmSession = + getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession); + } + inputFormat = newFormat; + + if (sourceDrmSession != decoderDrmSession) { + if (decoderReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; + } else { + // There aren't any final output buffers, so release the decoder immediately. + releaseDecoder(); + maybeInitDecoder(); + } + } + + eventDispatcher.inputFormatChanged(inputFormat); + } + + /** + * Called immediately before an input buffer is queued into the decoder. + * + *

The default implementation is a no-op. + * + * @param buffer The buffer that will be queued. + */ + protected void onQueueInputBuffer(VideoDecoderInputBuffer buffer) { + // Do nothing. + } + + /** + * Called when an output buffer is successfully processed. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + @CallSuper + protected void onProcessedOutputBuffer(long presentationTimeUs) { + buffersInCodecCount--; + } + + /** + * Returns whether the buffer being processed should be dropped. + * + * @param earlyUs The time until the buffer should be presented in microseconds. A negative value + * indicates that the buffer is late. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + */ + protected boolean shouldDropOutputBuffer(long earlyUs, long elapsedRealtimeUs) { + return isBufferLate(earlyUs); + } + + /** + * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after + * the current playback position, if possible. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + */ + protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { + return isBufferVeryLate(earlyUs); + } + + /** + * Returns whether to force rendering an output buffer. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + * @param elapsedSinceLastRenderUs The elapsed time since the last output buffer was rendered, in + * microseconds. + * @return Returns whether to force rendering an output buffer. + */ + protected boolean shouldForceRenderOutputBuffer(long earlyUs, long elapsedSinceLastRenderUs) { + return isBufferLate(earlyUs) && elapsedSinceLastRenderUs > 100000; + } + + /** + * Skips the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to skip. + */ + protected void skipOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + decoderCounters.skippedOutputBufferCount++; + outputBuffer.release(); + } + + /** + * Drops the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to drop. + */ + protected void dropOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + updateDroppedBufferCounters(1); + outputBuffer.release(); + } + + /** + * Drops frames from the current output buffer to the next keyframe at or before the playback + * position. If no such keyframe exists, as the playback position is inside the same group of + * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. + * + * @param positionUs The current playback position, in microseconds. + * @return Whether any buffers were dropped. + * @throws ExoPlaybackException If an error occurs flushing the decoder. + */ + protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { + int droppedSourceBufferCount = skipSource(positionUs); + if (droppedSourceBufferCount == 0) { + return false; + } + decoderCounters.droppedToKeyframeCount++; + // We dropped some buffers to catch up, so update the decoder counters and flush the decoder, + // which releases all pending buffers buffers including the current output buffer. + updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); + flushDecoder(); + return true; + } + + /** + * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were + * dropped. + * + * @param droppedBufferCount The number of additional dropped buffers. + */ + protected void updateDroppedBufferCounters(int droppedBufferCount) { + decoderCounters.droppedBufferCount += droppedBufferCount; + droppedFrames += droppedBufferCount; + consecutiveDroppedFrameCount += droppedBufferCount; + decoderCounters.maxConsecutiveDroppedBufferCount = + Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); + if (maxDroppedFramesToNotify > 0 && droppedFrames >= maxDroppedFramesToNotify) { + maybeNotifyDroppedFrames(); + } + } + + /** + * Returns the {@link Capabilities} for the given {@link Format}. + * + * @param drmSessionManager The renderer's {@link DrmSessionManager}. + * @param format The format, which has a video {@link Format#sampleMimeType}. + * @return The {@link Capabilities} for this {@link Format}. + * @see RendererCapabilities#supportsFormat(Format) + */ + @Capabilities + protected abstract int supportsFormatInternal( + @Nullable DrmSessionManager drmSessionManager, Format format); + + /** + * Creates a decoder for the given format. + * + * @param format The format for which a decoder is required. + * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content. + * May be null and can be ignored if decoder does not handle encrypted content. + * @return The decoder. + * @throws VideoDecoderException If an error occurred creating a suitable decoder. + */ + protected abstract SimpleDecoder< + VideoDecoderInputBuffer, + ? extends VideoDecoderOutputBuffer, + ? extends VideoDecoderException> + createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) + throws VideoDecoderException; + + /** + * Renders the specified output buffer. + * + *

The implementation of this method takes ownership of the output buffer and is responsible + * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future. + * + * @param outputBuffer {@link VideoDecoderOutputBuffer} to render. + * @param presentationTimeUs Presentation time in microseconds. + * @param outputFormat Output {@link Format}. + * @throws VideoDecoderException If an error occurs when rendering the output buffer. + */ + protected void renderOutputBuffer( + VideoDecoderOutputBuffer outputBuffer, long presentationTimeUs, Format outputFormat) + throws VideoDecoderException { + lastRenderTimeUs = C.msToUs(SystemClock.elapsedRealtime() * 1000); + int bufferMode = outputBuffer.mode; + boolean renderSurface = bufferMode == C.VIDEO_OUTPUT_MODE_SURFACE_YUV && surface != null; + boolean renderYuv = bufferMode == C.VIDEO_OUTPUT_MODE_YUV && outputBufferRenderer != null; + if (!renderYuv && !renderSurface) { + dropOutputBuffer(outputBuffer); + } else { + maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); + if (renderYuv) { + outputBufferRenderer.setOutputBuffer(outputBuffer); + } else { + renderOutputBufferToSurface(outputBuffer, surface); + } + consecutiveDroppedFrameCount = 0; + decoderCounters.renderedOutputBufferCount++; + maybeNotifyRenderedFirstFrame(); + } + } + + /** + * Renders the specified output buffer to the passed surface. + * + *

The implementation of this method takes ownership of the output buffer and is responsible + * for calling {@link VideoDecoderOutputBuffer#release()} either immediately or in the future. + * + * @param outputBuffer {@link VideoDecoderOutputBuffer} to render. + * @param surface Output {@link Surface}. + * @throws VideoDecoderException If an error occurs when rendering the output buffer. + */ + protected abstract void renderOutputBufferToSurface( + VideoDecoderOutputBuffer outputBuffer, Surface surface) throws VideoDecoderException; + + /** + * Sets output surface. + * + * @param surface Surface. + */ + protected final void setOutputSurface(@Nullable Surface surface) { + if (this.surface != surface) { + // The output has changed. + this.surface = surface; + if (surface != null) { + outputBufferRenderer = null; + outputMode = C.VIDEO_OUTPUT_MODE_SURFACE_YUV; + if (decoder != null) { + setDecoderOutputMode(outputMode); + } + onOutputChanged(); + } else { + // The output has been removed. We leave the outputMode of the underlying decoder unchanged + // in anticipation that a subsequent output will likely be of the same type. + outputMode = C.VIDEO_OUTPUT_MODE_NONE; + onOutputRemoved(); + } + } else if (surface != null) { + // The output is unchanged and non-null. + onOutputReset(); + } + } + + /** + * Sets output buffer renderer. + * + * @param outputBufferRenderer Output buffer renderer. + */ + protected final void setOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer outputBufferRenderer) { + if (this.outputBufferRenderer != outputBufferRenderer) { + // The output has changed. + this.outputBufferRenderer = outputBufferRenderer; + if (outputBufferRenderer != null) { + surface = null; + outputMode = C.VIDEO_OUTPUT_MODE_YUV; + if (decoder != null) { + setDecoderOutputMode(outputMode); + } + onOutputChanged(); + } else { + // The output has been removed. We leave the outputMode of the underlying decoder unchanged + // in anticipation that a subsequent output will likely be of the same type. + outputMode = C.VIDEO_OUTPUT_MODE_NONE; + onOutputRemoved(); + } + } else if (outputBufferRenderer != null) { + // The output is unchanged and non-null. + onOutputReset(); + } + } + + /** + * Sets output mode of the decoder. + * + * @param outputMode Output mode. + */ + protected abstract void setDecoderOutputMode(@C.VideoOutputMode int outputMode); + + // Internal methods. + + private void setSourceDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSession(sourceDrmSession, session); + sourceDrmSession = session; + } + + private void setDecoderDrmSession(@Nullable DrmSession session) { + DrmSession.replaceSession(decoderDrmSession, session); + decoderDrmSession = session; + } + + private void maybeInitDecoder() throws ExoPlaybackException { + if (decoder != null) { + return; + } + + setDecoderDrmSession(sourceDrmSession); + + ExoMediaCrypto mediaCrypto = null; + if (decoderDrmSession != null) { + mediaCrypto = decoderDrmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = decoderDrmSession.getError(); + if (drmError != null) { + // Continue for now. We may be able to avoid failure if the session recovers, or if a new + // input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; + } + } + } + + try { + long decoderInitializingTimestamp = SystemClock.elapsedRealtime(); + decoder = createDecoder(inputFormat, mediaCrypto); + setDecoderOutputMode(outputMode); + long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); + onDecoderInitialized( + decoder.getName(), + decoderInitializedTimestamp, + decoderInitializedTimestamp - decoderInitializingTimestamp); + decoderCounters.decoderInitCount++; + } catch (VideoDecoderException e) { + throw createRendererException(e, inputFormat); + } + } + + private boolean feedInputBuffer() throws VideoDecoderException, ExoPlaybackException { + if (decoder == null + || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM + || inputStreamEnded) { + // We need to reinitialize the decoder or the input stream has ended. + return false; + } + + if (inputBuffer == null) { + inputBuffer = decoder.dequeueInputBuffer(); + if (inputBuffer == null) { + return false; + } + } + + if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + return false; + } + + int result; + FormatHolder formatHolder = getFormatHolder(); + if (waitingForKeys) { + // We've already read an encrypted sample into buffer, and are waiting for keys. + result = C.RESULT_BUFFER_READ; + } else { + result = readSource(formatHolder, inputBuffer, false); + } + + if (result == C.RESULT_NOTHING_READ) { + return false; + } + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder); + return true; + } + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + return false; + } + boolean bufferEncrypted = inputBuffer.isEncrypted(); + waitingForKeys = shouldWaitForKeys(bufferEncrypted); + if (waitingForKeys) { + return false; + } + if (waitingForFirstSampleInFormat) { + formatQueue.add(inputBuffer.timeUs, inputFormat); + waitingForFirstSampleInFormat = false; + } + inputBuffer.flip(); + inputBuffer.colorInfo = inputFormat.colorInfo; + onQueueInputBuffer(inputBuffer); + decoder.queueInputBuffer(inputBuffer); + buffersInCodecCount++; + decoderReceivedBuffers = true; + decoderCounters.inputBufferCount++; + inputBuffer = null; + return true; + } + + /** + * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link + * #processOutputBuffer(long, long)}. + * + * @param positionUs The player's current position. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @return Whether it may be possible to drain more output data. + * @throws ExoPlaybackException If an error occurs draining the output buffer. + */ + private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException, VideoDecoderException { + if (outputBuffer == null) { + outputBuffer = decoder.dequeueOutputBuffer(); + if (outputBuffer == null) { + return false; + } + decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; + } + + if (outputBuffer.isEndOfStream()) { + if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // We're waiting to re-initialize the decoder, and have now processed all final buffers. + releaseDecoder(); + maybeInitDecoder(); + } else { + outputBuffer.release(); + outputBuffer = null; + outputStreamEnded = true; + } + return false; + } + + boolean processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs); + if (processedOutputBuffer) { + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + } + return processedOutputBuffer; + } + + /** + * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns + * whether it may be possible to process another output buffer. + * + * @param positionUs The player's current position. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @return Whether it may be possible to drain another output buffer. + * @throws ExoPlaybackException If an error occurs processing the output buffer. + */ + private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException, VideoDecoderException { + if (initialPositionUs == C.TIME_UNSET) { + initialPositionUs = positionUs; + } + + long earlyUs = outputBuffer.timeUs - positionUs; + if (!hasOutput()) { + // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. + if (isBufferLate(earlyUs)) { + skipOutputBuffer(outputBuffer); + return true; + } + return false; + } + + long presentationTimeUs = outputBuffer.timeUs - outputStreamOffsetUs; + Format format = formatQueue.pollFloor(presentationTimeUs); + if (format != null) { + outputFormat = format; + } + + long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000; + boolean isStarted = getState() == STATE_STARTED; + if (!renderedFirstFrame + || (isStarted + && shouldForceRenderOutputBuffer(earlyUs, elapsedRealtimeNowUs - lastRenderTimeUs))) { + renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); + return true; + } + + if (!isStarted || positionUs == initialPositionUs) { + return false; + } + + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) + && maybeDropBuffersToKeyframe(positionUs)) { + return false; + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { + dropOutputBuffer(outputBuffer); + return true; + } + + if (earlyUs < 30000) { + renderOutputBuffer(outputBuffer, presentationTimeUs, outputFormat); + return true; + } + + return false; + } + + private boolean hasOutput() { + return outputMode != C.VIDEO_OUTPUT_MODE_NONE; + } + + private void onOutputChanged() { + // If we know the video size, report it again immediately. + maybeRenotifyVideoSizeChanged(); + // We haven't rendered to the new output yet. + clearRenderedFirstFrame(); + if (getState() == STATE_STARTED) { + setJoiningDeadlineMs(); + } + } + + private void onOutputRemoved() { + clearReportedVideoSize(); + clearRenderedFirstFrame(); + } + + private void onOutputReset() { + // The output is unchanged and non-null. If we know the video size and/or have already + // rendered to the output, report these again immediately. + maybeRenotifyVideoSizeChanged(); + maybeRenotifyRenderedFirstFrame(); + } + + private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + if (decoderDrmSession == null + || (!bufferEncrypted + && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) { + return false; + } + @DrmSession.State int drmSessionState = decoderDrmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw createRendererException(decoderDrmSession.getError(), inputFormat); + } + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; + } + + private void setJoiningDeadlineMs() { + joiningDeadlineMs = + allowedJoiningTimeMs > 0 + ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) + : C.TIME_UNSET; + } + + private void clearRenderedFirstFrame() { + renderedFirstFrame = false; + } + + private void maybeNotifyRenderedFirstFrame() { + if (!renderedFirstFrame) { + renderedFirstFrame = true; + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void maybeRenotifyRenderedFirstFrame() { + if (renderedFirstFrame) { + eventDispatcher.renderedFirstFrame(surface); + } + } + + private void clearReportedVideoSize() { + reportedWidth = Format.NO_VALUE; + reportedHeight = Format.NO_VALUE; + } + + private void maybeNotifyVideoSizeChanged(int width, int height) { + if (reportedWidth != width || reportedHeight != height) { + reportedWidth = width; + reportedHeight = height; + eventDispatcher.videoSizeChanged( + width, height, /* unappliedRotationDegrees= */ 0, /* pixelWidthHeightRatio= */ 1); + } + } + + private void maybeRenotifyVideoSizeChanged() { + if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { + eventDispatcher.videoSizeChanged( + reportedWidth, + reportedHeight, + /* unappliedRotationDegrees= */ 0, + /* pixelWidthHeightRatio= */ 1); + } + } + + private void maybeNotifyDroppedFrames() { + if (droppedFrames > 0) { + long now = SystemClock.elapsedRealtime(); + long elapsedMs = now - droppedFrameAccumulationStartTimeMs; + eventDispatcher.droppedFrames(droppedFrames, elapsedMs); + droppedFrames = 0; + droppedFrameAccumulationStartTimeMs = now; + } + } + + private static boolean isBufferLate(long earlyUs) { + // Class a buffer as late if it should have been presented more than 30 ms ago. + return earlyUs < -30000; + } + + private static boolean isBufferVeryLate(long earlyUs) { + // Class a buffer as very late if it should have been presented more than 500 ms ago. + return earlyUs < -500000; + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java new file mode 100644 index 000000000..68108af63 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderException.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.video; + +/** Thrown when a video decoder error occurs. */ +public class VideoDecoderException extends Exception { + + /** + * Creates an instance with the given message. + * + * @param message The detail message for this exception. + */ + public VideoDecoderException(String message) { + super(message); + } + + /** + * Creates an instance with the given message and cause. + * + * @param message The detail message for this exception. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. + */ + public VideoDecoderException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java new file mode 100644 index 000000000..99f3d07b6 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderGLSurfaceView.java @@ -0,0 +1,57 @@ +/* + * 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 com.google.android.exoplayer2.video; + +import android.content.Context; +import android.opengl.GLSurfaceView; +import android.util.AttributeSet; +import androidx.annotation.Nullable; + +/** + * GLSurfaceView for rendering video output. To render video in this view, call {@link + * #getVideoDecoderOutputBufferRenderer()} to get a {@link VideoDecoderOutputBufferRenderer} that + * will render video decoder output buffers in this view. + * + *

This view is intended for use only with extension renderers. For other use cases a {@link + * android.view.SurfaceView} or {@link android.view.TextureView} should be used instead. + */ +public class VideoDecoderGLSurfaceView extends GLSurfaceView { + + private final VideoDecoderRenderer renderer; + + /** @param context A {@link Context}. */ + public VideoDecoderGLSurfaceView(Context context) { + this(context, /* attrs= */ null); + } + + /** + * @param context A {@link Context}. + * @param attrs Custom attributes. + */ + public VideoDecoderGLSurfaceView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + renderer = new VideoDecoderRenderer(this); + setPreserveEGLContextOnPause(true); + setEGLContextClientVersion(2); + setRenderer(renderer); + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + + /** Returns the {@link VideoDecoderOutputBufferRenderer} that will render frames in this view. */ + public VideoDecoderOutputBufferRenderer getVideoDecoderOutputBufferRenderer() { + return renderer; + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java new file mode 100644 index 000000000..360279c11 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderInputBuffer.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.video; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; + +/** Input buffer to a video decoder. */ +public class VideoDecoderInputBuffer extends DecoderInputBuffer { + + @Nullable public ColorInfo colorInfo; + + public VideoDecoderInputBuffer() { + super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); + } + +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java new file mode 100644 index 000000000..457aa30ad --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.video; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.decoder.OutputBuffer; +import java.nio.ByteBuffer; + +/** Video decoder output buffer containing video frame data. */ +public class VideoDecoderOutputBuffer extends OutputBuffer { + + /** Buffer owner. */ + public interface Owner { + + /** + * Releases the buffer. + * + * @param outputBuffer Output buffer. + */ + void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer); + } + + // LINT.IfChange + public static final int COLORSPACE_UNKNOWN = 0; + public static final int COLORSPACE_BT601 = 1; + public static final int COLORSPACE_BT709 = 2; + public static final int COLORSPACE_BT2020 = 3; + // LINT.ThenChange( + // ../../../../../../../../../../extensions/av1/src/main/jni/gav1_jni.cc, + // ../../../../../../../../../../extensions/vp9/src/main/jni/vpx_jni.cc + // ) + + /** Decoder private data. */ + public int decoderPrivate; + + /** Output mode. */ + @C.VideoOutputMode public int mode; + /** RGB buffer for RGB mode. */ + @Nullable public ByteBuffer data; + + public int width; + public int height; + @Nullable public ColorInfo colorInfo; + + /** YUV planes for YUV mode. */ + @Nullable public ByteBuffer[] yuvPlanes; + + @Nullable public int[] yuvStrides; + public int colorspace; + + /** + * Supplemental data related to the output frame, if {@link #hasSupplementalData()} returns true. + * If present, the buffer is populated with supplemental data from position 0 to its limit. + */ + @Nullable public ByteBuffer supplementalData; + + private final Owner owner; + + /** + * Creates VideoDecoderOutputBuffer. + * + * @param owner Buffer owner. + */ + public VideoDecoderOutputBuffer(Owner owner) { + this.owner = owner; + } + + @Override + public void release() { + owner.releaseOutputBuffer(this); + } + + /** + * Initializes the buffer. + * + * @param timeUs The presentation timestamp for the buffer, in microseconds. + * @param mode The output mode. One of {@link C#VIDEO_OUTPUT_MODE_NONE}, {@link + * C#VIDEO_OUTPUT_MODE_YUV} and {@link C#VIDEO_OUTPUT_MODE_SURFACE_YUV}. + * @param supplementalData Supplemental data associated with the frame, or {@code null} if not + * present. It is safe to reuse the provided buffer after this method returns. + */ + public void init( + long timeUs, @C.VideoOutputMode int mode, @Nullable ByteBuffer supplementalData) { + this.timeUs = timeUs; + this.mode = mode; + if (supplementalData != null && supplementalData.hasRemaining()) { + addFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA); + int size = supplementalData.limit(); + if (this.supplementalData == null || this.supplementalData.capacity() < size) { + this.supplementalData = ByteBuffer.allocate(size); + } else { + this.supplementalData.clear(); + } + this.supplementalData.put(supplementalData); + this.supplementalData.flip(); + supplementalData.position(0); + } else { + this.supplementalData = null; + } + } + + /** + * Resizes the buffer based on the given stride. Called via JNI after decoding completes. + * + * @return Whether the buffer was resized successfully. + */ + public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) { + this.width = width; + this.height = height; + this.colorspace = colorspace; + int uvHeight = (int) (((long) height + 1) / 2); + if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) { + return false; + } + int yLength = yStride * height; + int uvLength = uvStride * uvHeight; + int minimumYuvSize = yLength + (uvLength * 2); + if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) { + return false; + } + + // Initialize data. + if (data == null || data.capacity() < minimumYuvSize) { + data = ByteBuffer.allocateDirect(minimumYuvSize); + } else { + data.position(0); + data.limit(minimumYuvSize); + } + + if (yuvPlanes == null) { + yuvPlanes = new ByteBuffer[3]; + } + + ByteBuffer data = this.data; + ByteBuffer[] yuvPlanes = this.yuvPlanes; + + // Rewrapping has to be done on every frame since the stride might have changed. + yuvPlanes[0] = data.slice(); + yuvPlanes[0].limit(yLength); + data.position(yLength); + yuvPlanes[1] = data.slice(); + yuvPlanes[1].limit(uvLength); + data.position(yLength + uvLength); + yuvPlanes[2] = data.slice(); + yuvPlanes[2].limit(uvLength); + if (yuvStrides == null) { + yuvStrides = new int[3]; + } + yuvStrides[0] = yStride; + yuvStrides[1] = uvStride; + yuvStrides[2] = uvStride; + return true; + } + + /** + * Configures the buffer for the given frame dimensions when passing actual frame data via {@link + * #decoderPrivate}. Called via JNI after decoding completes. + */ + public void initForPrivateFrame(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Ensures that the result of multiplying individual numbers can fit into the size limit of an + * integer. + */ + private static boolean isSafeToMultiply(int a, int b) { + return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java new file mode 100644 index 000000000..c57794f45 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderOutputBufferRenderer.java @@ -0,0 +1,27 @@ +/* + * 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 com.google.android.exoplayer2.video; + +/** Renders the {@link VideoDecoderOutputBuffer}. */ +public interface VideoDecoderOutputBufferRenderer { + + /** + * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer. + * + * @param outputBuffer The output buffer to be rendered. + */ + void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer); +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java new file mode 100644 index 000000000..cb9c4eb59 --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoDecoderRenderer.java @@ -0,0 +1,241 @@ +/* + * 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 com.google.android.exoplayer2.video; + +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.GlUtil; +import java.nio.FloatBuffer; +import java.util.concurrent.atomic.AtomicReference; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * GLSurfaceView.Renderer implementation that can render YUV Frames returned by a video decoder + * after decoding. It does the YUV to RGB color conversion in the Fragment Shader. + */ +/* package */ class VideoDecoderRenderer + implements GLSurfaceView.Renderer, VideoDecoderOutputBufferRenderer { + + private static final float[] kColorConversion601 = { + 1.164f, 1.164f, 1.164f, + 0.0f, -0.392f, 2.017f, + 1.596f, -0.813f, 0.0f, + }; + + private static final float[] kColorConversion709 = { + 1.164f, 1.164f, 1.164f, + 0.0f, -0.213f, 2.112f, + 1.793f, -0.533f, 0.0f, + }; + + private static final float[] kColorConversion2020 = { + 1.168f, 1.168f, 1.168f, + 0.0f, -0.188f, 2.148f, + 1.683f, -0.652f, 0.0f, + }; + + private static final String VERTEX_SHADER = + "varying vec2 interp_tc_y;\n" + + "varying vec2 interp_tc_u;\n" + + "varying vec2 interp_tc_v;\n" + + "attribute vec4 in_pos;\n" + + "attribute vec2 in_tc_y;\n" + + "attribute vec2 in_tc_u;\n" + + "attribute vec2 in_tc_v;\n" + + "void main() {\n" + + " gl_Position = in_pos;\n" + + " interp_tc_y = in_tc_y;\n" + + " interp_tc_u = in_tc_u;\n" + + " interp_tc_v = in_tc_v;\n" + + "}\n"; + private static final String[] TEXTURE_UNIFORMS = {"y_tex", "u_tex", "v_tex"}; + private static final String FRAGMENT_SHADER = + "precision mediump float;\n" + + "varying vec2 interp_tc_y;\n" + + "varying vec2 interp_tc_u;\n" + + "varying vec2 interp_tc_v;\n" + + "uniform sampler2D y_tex;\n" + + "uniform sampler2D u_tex;\n" + + "uniform sampler2D v_tex;\n" + + "uniform mat3 mColorConversion;\n" + + "void main() {\n" + + " vec3 yuv;\n" + + " yuv.x = texture2D(y_tex, interp_tc_y).r - 0.0625;\n" + + " yuv.y = texture2D(u_tex, interp_tc_u).r - 0.5;\n" + + " yuv.z = texture2D(v_tex, interp_tc_v).r - 0.5;\n" + + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n" + + "}\n"; + + private static final FloatBuffer TEXTURE_VERTICES = + GlUtil.createBuffer(new float[] {-1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f}); + private final GLSurfaceView surfaceView; + private final int[] yuvTextures = new int[3]; + private final AtomicReference pendingOutputBufferReference; + + // Kept in field rather than a local variable in order not to get garbage collected before + // glDrawArrays uses it. + private FloatBuffer[] textureCoords; + + private int program; + private int[] texLocations; + private int colorMatrixLocation; + private int[] previousWidths; + private int[] previousStrides; + + @Nullable + private VideoDecoderOutputBuffer renderedOutputBuffer; // Accessed only from the GL thread. + + public VideoDecoderRenderer(GLSurfaceView surfaceView) { + this.surfaceView = surfaceView; + pendingOutputBufferReference = new AtomicReference<>(); + textureCoords = new FloatBuffer[3]; + texLocations = new int[3]; + previousWidths = new int[3]; + previousStrides = new int[3]; + for (int i = 0; i < 3; i++) { + previousWidths[i] = previousStrides[i] = -1; + } + } + + @Override + public void onSurfaceCreated(GL10 unused, EGLConfig config) { + program = GlUtil.compileProgram(VERTEX_SHADER, FRAGMENT_SHADER); + GLES20.glUseProgram(program); + int posLocation = GLES20.glGetAttribLocation(program, "in_pos"); + GLES20.glEnableVertexAttribArray(posLocation); + GLES20.glVertexAttribPointer(posLocation, 2, GLES20.GL_FLOAT, false, 0, TEXTURE_VERTICES); + texLocations[0] = GLES20.glGetAttribLocation(program, "in_tc_y"); + GLES20.glEnableVertexAttribArray(texLocations[0]); + texLocations[1] = GLES20.glGetAttribLocation(program, "in_tc_u"); + GLES20.glEnableVertexAttribArray(texLocations[1]); + texLocations[2] = GLES20.glGetAttribLocation(program, "in_tc_v"); + GLES20.glEnableVertexAttribArray(texLocations[2]); + GlUtil.checkGlError(); + colorMatrixLocation = GLES20.glGetUniformLocation(program, "mColorConversion"); + GlUtil.checkGlError(); + setupTextures(); + GlUtil.checkGlError(); + } + + @Override + public void onSurfaceChanged(GL10 unused, int width, int height) { + GLES20.glViewport(0, 0, width, height); + } + + @Override + public void onDrawFrame(GL10 unused) { + VideoDecoderOutputBuffer pendingOutputBuffer = pendingOutputBufferReference.getAndSet(null); + if (pendingOutputBuffer == null && renderedOutputBuffer == null) { + // There is no output buffer to render at the moment. + return; + } + if (pendingOutputBuffer != null) { + if (renderedOutputBuffer != null) { + renderedOutputBuffer.release(); + } + renderedOutputBuffer = pendingOutputBuffer; + } + VideoDecoderOutputBuffer outputBuffer = renderedOutputBuffer; + // Set color matrix. Assume BT709 if the color space is unknown. + float[] colorConversion = kColorConversion709; + switch (outputBuffer.colorspace) { + case VideoDecoderOutputBuffer.COLORSPACE_BT601: + colorConversion = kColorConversion601; + break; + case VideoDecoderOutputBuffer.COLORSPACE_BT2020: + colorConversion = kColorConversion2020; + break; + case VideoDecoderOutputBuffer.COLORSPACE_BT709: + default: + break; // Do nothing + } + GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0); + + for (int i = 0; i < 3; i++) { + int h = (i == 0) ? outputBuffer.height : (outputBuffer.height + 1) / 2; + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); + GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, + 0, + GLES20.GL_LUMINANCE, + outputBuffer.yuvStrides[i], + h, + 0, + GLES20.GL_LUMINANCE, + GLES20.GL_UNSIGNED_BYTE, + outputBuffer.yuvPlanes[i]); + } + + int[] widths = new int[3]; + widths[0] = outputBuffer.width; + // TODO: Handle streams where chroma channels are not stored at half width and height + // compared to luma channel. See [Internal: b/142097774]. + // U and V planes are being stored at half width compared to Y. + widths[1] = widths[2] = (widths[0] + 1) / 2; + for (int i = 0; i < 3; i++) { + // Set cropping of stride if either width or stride has changed. + if (previousWidths[i] != widths[i] || previousStrides[i] != outputBuffer.yuvStrides[i]) { + Assertions.checkState(outputBuffer.yuvStrides[i] != 0); + float widthRatio = (float) widths[i] / outputBuffer.yuvStrides[i]; + // These buffers are consumed during each call to glDrawArrays. They need to be member + // variables rather than local variables in order not to get garbage collected. + textureCoords[i] = + GlUtil.createBuffer( + new float[] {0.0f, 0.0f, 0.0f, 1.0f, widthRatio, 0.0f, widthRatio, 1.0f}); + GLES20.glVertexAttribPointer( + texLocations[i], 2, GLES20.GL_FLOAT, false, 0, textureCoords[i]); + previousWidths[i] = widths[i]; + previousStrides[i] = outputBuffer.yuvStrides[i]; + } + } + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + GlUtil.checkGlError(); + } + + @Override + public void setOutputBuffer(VideoDecoderOutputBuffer outputBuffer) { + VideoDecoderOutputBuffer oldPendingOutputBuffer = + pendingOutputBufferReference.getAndSet(outputBuffer); + if (oldPendingOutputBuffer != null) { + // The old pending output buffer will never be used for rendering, so release it now. + oldPendingOutputBuffer.release(); + } + surfaceView.requestRender(); + } + + private void setupTextures() { + GLES20.glGenTextures(3, yuvTextures, 0); + for (int i = 0; i < 3; i++) { + GLES20.glUniform1i(GLES20.glGetUniformLocation(program, TEXTURE_UNIFORMS[i]), i); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + } + GlUtil.checkGlError(); + } +} diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java index b467d0f42..746903a10 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoFrameMetadataListener.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.video; +import android.media.MediaFormat; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; /** A listener for metadata corresponding to video frame being rendered. */ @@ -26,6 +28,13 @@ public interface VideoFrameMetadataListener { * @param releaseTimeNs The wallclock time at which the frame should be displayed, in nanoseconds. * If the platform API version of the device is less than 21, then this is the best effort. * @param format The format associated with the frame. + * @param mediaFormat The framework media format associated with the frame, or {@code null} if not + * known or not applicable (e.g., because the frame was not output by a {@link + * android.media.MediaCodec MediaCodec}). */ - void onVideoFrameAboutToBeRendered(long presentationTimeUs, long releaseTimeNs, Format format); + void onVideoFrameAboutToBeRendered( + long presentationTimeUs, + long releaseTimeNs, + Format format, + @Nullable MediaFormat mediaFormat); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index caa79d7c1..bf31ce2ab 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -21,11 +21,11 @@ import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; -import androidx.annotation.Nullable; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; import android.view.Display; import android.view.WindowManager; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 2f76a2c23..e7dfd123b 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -15,11 +15,13 @@ */ package com.google.android.exoplayer2.video; +import static com.google.android.exoplayer2.util.Util.castNonNull; + import android.os.Handler; import android.os.SystemClock; -import androidx.annotation.Nullable; import android.view.Surface; import android.view.TextureView; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderCounters; @@ -126,33 +128,34 @@ public interface VideoRendererEventListener { /** Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}. */ public void enabled(DecoderCounters decoderCounters) { - if (listener != null) { - handler.post(() -> listener.onVideoEnabled(decoderCounters)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onVideoEnabled(decoderCounters)); } } /** Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}. */ public void decoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onVideoDecoderInitialized( - decoderName, initializedTimestampMs, initializationDurationMs)); + castNonNull(listener) + .onVideoDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs)); } } /** Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}. */ public void inputFormatChanged(Format format) { - if (listener != null) { - handler.post(() -> listener.onVideoInputFormatChanged(format)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onVideoInputFormatChanged(format)); } } /** Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ public void droppedFrames(int droppedFrameCount, long elapsedMs) { - if (listener != null) { - handler.post(() -> listener.onDroppedFrames(droppedFrameCount, elapsedMs)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onDroppedFrames(droppedFrameCount, elapsedMs)); } } @@ -162,29 +165,30 @@ public interface VideoRendererEventListener { int height, final int unappliedRotationDegrees, final float pixelWidthHeightRatio) { - if (listener != null) { + if (handler != null) { handler.post( () -> - listener.onVideoSizeChanged( - width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); + castNonNull(listener) + .onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio)); } } /** Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}. */ public void renderedFirstFrame(@Nullable Surface surface) { - if (listener != null) { - handler.post(() -> listener.onRenderedFirstFrame(surface)); + if (handler != null) { + handler.post(() -> castNonNull(listener).onRenderedFirstFrame(surface)); } } /** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */ public void disabled(DecoderCounters counters) { counters.ensureUpdated(); - if (listener != null) { + if (handler != null) { handler.post( () -> { counters.ensureUpdated(); - listener.onVideoDisabled(counters); + castNonNull(listener).onVideoDisabled(counters); }); } } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/package-info.java new file mode 100644 index 000000000..3c2cd217e --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.video; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java index eb7110834..35804adbe 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -34,26 +35,25 @@ public class CameraMotionRenderer extends BaseRenderer { // The amount of time to read samples ahead of the current time. private static final int SAMPLE_WINDOW_DURATION_US = 100000; - private final FormatHolder formatHolder; private final DecoderInputBuffer buffer; private final ParsableByteArray scratch; private long offsetUs; - private @Nullable CameraMotionListener listener; + @Nullable private CameraMotionListener listener; private long lastTimestampUs; public CameraMotionRenderer() { super(C.TRACK_TYPE_CAMERA_MOTION); - formatHolder = new FormatHolder(); buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); scratch = new ParsableByteArray(); } @Override + @Capabilities public int supportsFormat(Format format) { return MimeTypes.APPLICATION_CAMERA_MOTION.equals(format.sampleMimeType) - ? FORMAT_HANDLED - : FORMAT_UNSUPPORTED_TYPE; + ? RendererCapabilities.create(FORMAT_HANDLED) + : RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE); } @Override @@ -85,6 +85,7 @@ public class CameraMotionRenderer extends BaseRenderer { // Keep reading available samples as long as the sample time is not too far into the future. while (!hasReadStreamToEnd() && lastTimestampUs < positionUs + SAMPLE_WINDOW_DURATION_US) { buffer.clear(); + FormatHolder formatHolder = getFormatHolder(); int result = readSource(formatHolder, buffer, /* formatRequired= */ false); if (result != C.RESULT_BUFFER_READ || buffer.isEndOfStream()) { return; @@ -93,7 +94,7 @@ public class CameraMotionRenderer extends BaseRenderer { buffer.flip(); lastTimestampUs = buffer.timeUs; if (listener != null) { - float[] rotation = parseMetadata(buffer.data); + float[] rotation = parseMetadata(Util.castNonNull(buffer.data)); if (rotation != null) { Util.castNonNull(listener).onCameraMotion(lastTimestampUs - offsetUs, rotation); } diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java index 527aa5db4..eadc617ea 100644 --- a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/ProjectionDecoder.java @@ -36,12 +36,12 @@ import java.util.zip.Inflater; */ public final class ProjectionDecoder { - private static final int TYPE_YTMP = Util.getIntegerCodeForString("ytmp"); - private static final int TYPE_MSHP = Util.getIntegerCodeForString("mshp"); - private static final int TYPE_RAW = Util.getIntegerCodeForString("raw "); - private static final int TYPE_DFL8 = Util.getIntegerCodeForString("dfl8"); - private static final int TYPE_MESH = Util.getIntegerCodeForString("mesh"); - private static final int TYPE_PROJ = Util.getIntegerCodeForString("proj"); + private static final int TYPE_YTMP = 0x79746d70; + private static final int TYPE_MSHP = 0x6d736870; + private static final int TYPE_RAW = 0x72617720; + private static final int TYPE_DFL8 = 0x64666c38; + private static final int TYPE_MESH = 0x6d657368; + private static final int TYPE_PROJ = 0x70726f6a; // Sanity limits to prevent a bad file from creating an OOM situation. We don't expect a mesh to // exceed these limits. diff --git a/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/package-info.java b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/package-info.java new file mode 100644 index 000000000..2dce6889d --- /dev/null +++ b/TMessagesProj/src/main/java/com/google/android/exoplayer2/video/spherical/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.video.spherical; + +import com.google.android.exoplayer2.util.NonNullApi; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java index c10d68836..3a737a414 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java @@ -47,6 +47,11 @@ import android.provider.CallLog; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.provider.Settings; + +import androidx.core.content.FileProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.viewpager.widget.ViewPager; + import android.telephony.TelephonyManager; import android.text.Selection; import android.text.Spannable; @@ -94,6 +99,7 @@ import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestTimeDelegate; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; +import org.telegram.ui.ActionBar.ActionBarLayout; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.BottomSheet; @@ -103,6 +109,7 @@ import org.telegram.ui.Components.BackgroundGradientDrawable; import org.telegram.ui.Components.ForegroundDetector; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.PickerBottomLayout; +import org.telegram.ui.Components.RecyclerListView; import org.telegram.ui.Components.ShareAlert; import org.telegram.ui.Components.TypefaceSpan; import org.telegram.ui.ThemePreviewActivity; @@ -502,8 +509,8 @@ public class AndroidUtilities { double rf = r / 255.0; double gf = g / 255.0; double bf = b / 255.0; - double max = (rf > gf && rf > bf) ? rf : (gf > bf) ? gf : bf; - double min = (rf < gf && rf < bf) ? rf : (gf < bf) ? gf : bf; + double max = (rf > gf && rf > bf) ? rf : Math.max(gf, bf); + double min = (rf < gf && rf < bf) ? rf : Math.min(gf, bf); double h, s; double d = max - min; s = max == 0 ? 0 : d / max; @@ -2600,6 +2607,25 @@ public class AndroidUtilities { return false; } + public static CharSequence replaceNewLines(CharSequence original) { + if (original instanceof StringBuilder) { + StringBuilder stringBuilder = (StringBuilder) original; + for (int a = 0, N = original.length(); a < N; a++) { + if (original.charAt(a) == '\n') { + stringBuilder.setCharAt(a, ' '); + } + } + } else if (original instanceof SpannableStringBuilder) { + SpannableStringBuilder stringBuilder = (SpannableStringBuilder) original; + for (int a = 0, N = original.length(); a < N; a++) { + if (original.charAt(a) == '\n') { + stringBuilder.replace(a, a + 1, " "); + } + } + } + return original.toString().replace('\n', ' '); + } + public static void openForView(TLObject media, Activity activity) { if (media == null || activity == null) { return; @@ -3413,11 +3439,11 @@ public class AndroidUtilities { public static float[] RGBtoHSB(int r, int g, int b) { float hue, saturation, brightness; float[] hsbvals = new float[3]; - int cmax = (r > g) ? r : g; + int cmax = Math.max(r, g); if (b > cmax) { cmax = b; } - int cmin = (r < g) ? r : g; + int cmin = Math.min(r, g); if (b < cmin) { cmin = b; } @@ -3573,7 +3599,7 @@ public class AndroidUtilities { public static float distanceInfluenceForSnapDuration(float f) { f -= 0.5F; f *= 0.47123894F; - return (float) Math.sin((double) f); + return (float) Math.sin(f); } public static void makeAccessibilityAnnouncement(CharSequence what) { @@ -3801,4 +3827,35 @@ public class AndroidUtilities { } return hasLatin && hasNonLatin; } + + public static void scrollToFragmentRow(ActionBarLayout parentLayout, String rowName) { + if (parentLayout == null || rowName == null) { + return; + } + BaseFragment openingFragment = parentLayout.fragmentsStack.get(parentLayout.fragmentsStack.size() - 1); + try { + Field listViewField = openingFragment.getClass().getDeclaredField("listView"); + listViewField.setAccessible(true); + RecyclerListView listView = (RecyclerListView) listViewField.get(openingFragment); + RecyclerListView.IntReturnCallback callback = () -> { + int position = -1; + try { + Field rowField = openingFragment.getClass().getDeclaredField(rowName); + rowField.setAccessible(true); + LinearLayoutManager layoutManager = (LinearLayoutManager) listView.getLayoutManager(); + position = rowField.getInt(openingFragment); + layoutManager.scrollToPositionWithOffset(position, AndroidUtilities.dp(60)); + rowField.setAccessible(false); + return position; + } catch (Throwable ignore) { + + } + return position; + }; + listView.highlightRow(callback); + listViewField.setAccessible(false); + } catch (Throwable ignore) { + + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimatedFileDrawableStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimatedFileDrawableStream.java index 2640aa6da..7a29311ec 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimatedFileDrawableStream.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AnimatedFileDrawableStream.java @@ -9,6 +9,7 @@ public class AnimatedFileDrawableStream implements FileLoadOperationStream { private FileLoadOperation loadOperation; private CountDownLatch countDownLatch; private TLRPC.Document document; + private ImageLocation location; private Object parentObject; private int currentAccount; private volatile boolean canceled; @@ -21,12 +22,13 @@ public class AnimatedFileDrawableStream implements FileLoadOperationStream { private boolean ignored; - public AnimatedFileDrawableStream(TLRPC.Document d, Object p, int a, boolean prev) { + public AnimatedFileDrawableStream(TLRPC.Document d, ImageLocation l, Object p, int a, boolean prev) { document = d; + location = l; parentObject = p; currentAccount = a; preview = prev; - loadOperation = FileLoader.getInstance(currentAccount).loadStreamFile(this, document, parentObject, 0, preview); + loadOperation = FileLoader.getInstance(currentAccount).loadStreamFile(this, document, location, parentObject, 0, preview); } public boolean isFinishedLoadingFile() { @@ -57,7 +59,7 @@ public class AnimatedFileDrawableStream implements FileLoadOperationStream { } if (availableLength == 0) { if (loadOperation.isPaused() || lastOffset != offset || preview) { - FileLoader.getInstance(currentAccount).loadStreamFile(this, document, parentObject, offset, preview); + FileLoader.getInstance(currentAccount).loadStreamFile(this, document, location, parentObject, offset, preview); } synchronized (sync) { if (canceled) { @@ -107,6 +109,10 @@ public class AnimatedFileDrawableStream implements FileLoadOperationStream { return document; } + public ImageLocation getLocation() { + return location; + } + public Object getParentObject() { return document; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java b/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java index 4f8520d6d..8cea5ac7f 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java @@ -32,6 +32,7 @@ import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; @@ -55,6 +56,7 @@ public class ContactsController extends BaseController { private boolean updatingInviteLink; private HashMap sectionsToReplace = new HashMap<>(); + private int loadingGlobalSettings; private int loadingDeleteInfo; private int deleteAccountTTL; private int[] loadingPrivacyInfo = new int[PRIVACY_RULES_TYPE_COUNT]; @@ -66,6 +68,7 @@ public class ContactsController extends BaseController { private ArrayList forwardsPrivacyRules; private ArrayList phonePrivacyRules; private ArrayList addedByPhonePrivacyRules; + private TLRPC.TL_globalPrivacySettings globalPrivacySettings; public final static int PRIVACY_RULES_TYPE_LASTSEEN = 0; public final static int PRIVACY_RULES_TYPE_INVITE = 1; @@ -253,11 +256,10 @@ public class ContactsController extends BaseController { contactsLoaded = false; contactsBookLoaded = false; lastContactsVersions = ""; + loadingGlobalSettings = 0; loadingDeleteInfo = 0; deleteAccountTTL = 0; - for (int a = 0; a < loadingPrivacyInfo.length; a++) { - loadingPrivacyInfo[a] = 0; - } + Arrays.fill(loadingPrivacyInfo, 0); lastseenPrivacyRules = null; groupPrivacyRules = null; callPrivacyRules = null; @@ -2308,6 +2310,19 @@ public class ContactsController extends BaseController { getNotificationCenter().postNotificationName(NotificationCenter.privacyRulesUpdated); })); } + if (loadingGlobalSettings == 0) { + loadingGlobalSettings = 1; + TLRPC.TL_account_getGlobalPrivacySettings req = new TLRPC.TL_account_getGlobalPrivacySettings(); + getConnectionsManager().sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> { + if (error == null) { + globalPrivacySettings = (TLRPC.TL_globalPrivacySettings) response; + loadingGlobalSettings = 2; + } else { + loadingGlobalSettings = 0; + } + getNotificationCenter().postNotificationName(NotificationCenter.privacyRulesUpdated); + })); + } for (int a = 0; a < loadingPrivacyInfo.length; a++) { if (loadingPrivacyInfo[a] != 0) { continue; @@ -2400,10 +2415,18 @@ public class ContactsController extends BaseController { return loadingDeleteInfo != 2; } + public boolean getLoadingGlobalSettings() { + return loadingGlobalSettings != 2; + } + public boolean getLoadingPrivicyInfo(int type) { return loadingPrivacyInfo[type] != 2; } + public TLRPC.TL_globalPrivacySettings getGlobalPrivacySettings() { + return globalPrivacySettings; + } + public ArrayList getPrivacyRules(int type) { switch (type) { case PRIVACY_RULES_TYPE_LASTSEEN: diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/DispatchQueue.java b/TMessagesProj/src/main/java/org/telegram/messenger/DispatchQueue.java index f2d386256..84ebc8f30 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/DispatchQueue.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/DispatchQueue.java @@ -65,21 +65,21 @@ public class DispatchQueue extends Thread { } } - public void postRunnable(Runnable runnable) { - postRunnable(runnable, 0); + public boolean postRunnable(Runnable runnable) { lastTaskTime = SystemClock.elapsedRealtime(); + return postRunnable(runnable, 0); } - public void postRunnable(Runnable runnable, long delay) { + public boolean postRunnable(Runnable runnable, long delay) { try { syncLatch.await(); } catch (Exception e) { FileLog.e(e); } if (delay <= 0) { - handler.post(runnable); + return handler.post(runnable); } else { - handler.postDelayed(runnable, delay); + return handler.postDelayed(runnable, delay); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/DownloadController.java b/TMessagesProj/src/main/java/org/telegram/messenger/DownloadController.java index adb65c45a..e1f2031e4 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/DownloadController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/DownloadController.java @@ -15,6 +15,7 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.net.ConnectivityManager; import android.util.LongSparseArray; +import android.util.Pair; import android.util.SparseArray; import org.telegram.tgnet.TLRPC; @@ -54,6 +55,7 @@ public class DownloadController extends BaseController implements NotificationCe private ArrayList documentDownloadQueue = new ArrayList<>(); private ArrayList videoDownloadQueue = new ArrayList<>(); private HashMap downloadQueueKeys = new HashMap<>(); + private HashMap, DownloadObject> downloadQueuePairs = new HashMap<>(); private HashMap>> loadingFileObservers = new HashMap<>(); private HashMap> loadingFileMessagesObservers = new HashMap<>(); @@ -413,6 +415,7 @@ public class DownloadController extends BaseController implements NotificationCe documentDownloadQueue.clear(); videoDownloadQueue.clear(); downloadQueueKeys.clear(); + downloadQueuePairs.clear(); typingTimes.clear(); } @@ -756,6 +759,26 @@ public class DownloadController extends BaseController implements NotificationCe }); } + protected void cancelDownloading(ArrayList> arrayList) { + for (int a = 0, N = arrayList.size(); a < N; a++) { + Pair pair = arrayList.get(a); + DownloadObject downloadObject = downloadQueuePairs.get(pair); + if (downloadObject == null) { + continue; + } + if (downloadObject.object instanceof TLRPC.Document) { + TLRPC.Document document = (TLRPC.Document) downloadObject.object; + getFileLoader().cancelLoadFile(document); + } else if (downloadObject.object instanceof TLRPC.Photo) { + TLRPC.Photo photo = (TLRPC.Photo) downloadObject.object; + TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, AndroidUtilities.getPhotoSize()); + if (photoSize != null) { + getFileLoader().cancelLoadFile(photoSize); + } + } + } + } + protected void processDownloadObjects(int type, ArrayList objects) { if (objects.isEmpty()) { return; @@ -808,6 +831,7 @@ public class DownloadController extends BaseController implements NotificationCe if (added) { queue.add(downloadObject); downloadQueueKeys.put(path, downloadObject); + downloadQueuePairs.put(new Pair<>(downloadObject.id, downloadObject.type), downloadObject); } } } @@ -832,6 +856,7 @@ public class DownloadController extends BaseController implements NotificationCe DownloadObject downloadObject = downloadQueueKeys.get(fileName); if (downloadObject != null) { downloadQueueKeys.remove(fileName); + downloadQueuePairs.remove(new Pair<>(downloadObject.id, downloadObject.type)); if (state == 0 || state == 2) { getMessagesStorage().removeFromDownloadQueue(downloadObject.id, downloadObject.type, false /*state != 0*/); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Emoji.java b/TMessagesProj/src/main/java/org/telegram/messenger/Emoji.java index 58b5b4b13..fc4f4c069 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Emoji.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/Emoji.java @@ -486,6 +486,9 @@ public class Emoji { FileLog.e(e); return cs; } + if (emojiOnly != null && emojiCode.length() != 0) { + emojiOnly[0] = 0; + } return s; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/EmojiData.java b/TMessagesProj/src/main/java/org/telegram/messenger/EmojiData.java index 5f1bc31a5..019ffffc5 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/EmojiData.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/EmojiData.java @@ -799,6 +799,10 @@ public class EmojiData { return "❤".equals(emoji) || "🧡".equals(emoji) || "💛".equals(emoji) || "💚".equals(emoji) || "💙".equals(emoji) || "💜".equals(emoji) || "🖤".equals(emoji) || "🤍".equals(emoji) || "🤎".equals(emoji); } + public static boolean isPeachEmoji(String emoji) { + return "\uD83C\uDF51".equals(emoji); + } + static { for (int a = 0; a < emojiToFE0F.length; a++) { emojiToFE0FMap.put(emojiToFE0F[a], true); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java b/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java index c376f4ed6..275a40f2b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/FileLoadOperation.java @@ -71,10 +71,13 @@ public class FileLoadOperation { private final static int maxDownloadRequests = 4; private final static int maxDownloadRequestsBig = 4; private final static int bigFileSizeFrom = 1024 * 1024; - private final static int maxCdnParts = 1024 * 1024 * 1536 / downloadChunkSizeBig; + private final static int maxCdnParts = (int) (FileLoader.MAX_FILE_SIZE / downloadChunkSizeBig); private final static int preloadMaxBytes = 2 * 1024 * 1024; + private String fileName; + private int currentQueueType; + private SparseArray preloadedBytesRanges; private SparseIntArray requestedPreloadedBytesRanges; private RandomAccessFile preloadStream; @@ -130,6 +133,8 @@ public class FileLoadOperation { private SparseArray cdnHashes; + private boolean forceBig; + private byte[] encryptKey; private byte[] encryptIv; @@ -147,8 +152,6 @@ public class FileLoadOperation { private ArrayList requestInfos; private ArrayList delayedRequestInfos; - private String fileName; - private File cacheFileTemp; private File cacheFileGzipTemp; private File cacheFileFinal; @@ -176,6 +179,7 @@ public class FileLoadOperation { public FileLoadOperation(ImageLocation imageLocation, Object parent, String extension, int size) { parentObject = parent; + forceBig = imageLocation.imageType == FileLoader.IMAGE_TYPE_ANIMATION; if (imageLocation.isEncrypted()) { location = new TLRPC.TL_inputEncryptedFileLocation(); location.id = imageLocation.location.volume_id; @@ -207,6 +211,9 @@ public class FileLoadOperation { location.access_hash = imageLocation.access_hash; location.file_reference = imageLocation.file_reference; location.thumb_size = imageLocation.thumbSize; + if (imageLocation.imageType == FileLoader.IMAGE_TYPE_ANIMATION) { + allowDisordererFileSave = true; + } } else { location = new TLRPC.TL_inputDocumentFileLocation(); location.id = imageLocation.documentId; @@ -268,8 +275,7 @@ public class FileLoadOperation { ext = ImageLoader.getHttpUrlExtension(webDocument.url, defaultExt); } - public FileLoadOperation(TLRPC.Document documentLocation, Object parent,String fileName) { - this.fileName = fileName; + public FileLoadOperation(TLRPC.Document documentLocation, Object parent) { try { parentObject = parent; if (documentLocation instanceof TLRPC.TL_documentEncrypted) { @@ -357,10 +363,16 @@ public class FileLoadOperation { return priority; } - public void setPaths(int instance, File store, File temp) { + public void setPaths(int instance, String name, int queueType, File store, File temp) { storePath = store; tempPath = temp; currentAccount = instance; + fileName = name; + currentQueueType = queueType; + } + + public int getQueueType() { + return currentQueueType; } public boolean wasStarted() { @@ -555,11 +567,7 @@ public class FileLoadOperation { } public String getFileName() { - if (location != null) { - return location.volume_id + "_" + location.local_id + "." + ext; - } else { - return Utilities.MD5(webFile.url) + "." + ext; - } + return fileName; } protected void removeStreamListener(final FileLoadOperationStream operation) { @@ -591,8 +599,8 @@ public class FileLoadOperation { public boolean start(final FileLoadOperationStream stream, final int streamOffset, final boolean steamPriority) { if (currentDownloadChunkSize == 0) { - currentDownloadChunkSize = totalBytesCount >= bigFileSizeFrom ? downloadChunkSizeBig : downloadChunkSize; - currentMaxDownloadRequests = totalBytesCount >= bigFileSizeFrom ? maxDownloadRequestsBig : maxDownloadRequests; + currentDownloadChunkSize = totalBytesCount >= bigFileSizeFrom || forceBig ? downloadChunkSizeBig : downloadChunkSize; + currentMaxDownloadRequests = totalBytesCount >= bigFileSizeFrom || forceBig ? maxDownloadRequestsBig : maxDownloadRequests; } final boolean alreadyStarted = state != stateIdle; final boolean wasPaused = paused; @@ -1451,7 +1459,7 @@ public class FileLoadOperation { if (location != null) { FileLog.e("invalid cdn hash " + location + " id = " + location.id + " local_id = " + location.local_id + " access_hash = " + location.access_hash + " volume_id = " + location.volume_id + " secret = " + location.secret); } else if (webLocation != null) { - FileLog.e("invalid cdn hash " + webLocation + " id = " + getFileName()); + FileLog.e("invalid cdn hash " + webLocation + " id = " + fileName); } } onFail(false, 0); @@ -1538,7 +1546,7 @@ public class FileLoadOperation { if (location != null) { FileLog.e(error.text + " " + location + " id = " + location.id + " local_id = " + location.local_id + " access_hash = " + location.access_hash + " volume_id = " + location.volume_id + " secret = " + location.secret); } else if (webLocation != null) { - FileLog.e(error.text + " " + webLocation + " id = " + getFileName()); + FileLog.e(error.text + " " + webLocation + " id = " + fileName); } } onFail(false, 0); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/FileLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/FileLoader.java index 741ec14dc..9dadb8255 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/FileLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/FileLoader.java @@ -48,6 +48,12 @@ public class FileLoader extends BaseController { public static final int IMAGE_TYPE_SVG_WHITE = 4; public static final int IMAGE_TYPE_THEME_PREVIEW = 5; + public static final int QUEUE_TYPE_FILE = 0; + public static final int QUEUE_TYPE_IMAGE = 1; + public static final int QUEUE_TYPE_AUDIO = 2; + + public final static long MAX_FILE_SIZE = 1024L * 1024L * 2000L; + private volatile static DispatchQueue fileLoaderQueue = new DispatchQueue("fileUploadQueue"); private LinkedList uploadOperationQueue = new LinkedList<>(); @@ -57,12 +63,12 @@ public class FileLoader extends BaseController { private int currentUploadOperationsCount = 0; private int currentUploadSmallOperationsCount = 0; - private SparseArray> loadOperationQueues = new SparseArray<>(); + private SparseArray> fileLoadOperationQueues = new SparseArray<>(); private SparseArray> audioLoadOperationQueues = new SparseArray<>(); - private SparseArray> photoLoadOperationQueues = new SparseArray<>(); - private SparseIntArray currentLoadOperationsCount = new SparseIntArray(); - private SparseIntArray currentAudioLoadOperationsCount = new SparseIntArray(); - private SparseIntArray currentPhotoLoadOperationsCount = new SparseIntArray(); + private SparseArray> imageLoadOperationQueues = new SparseArray<>(); + private SparseIntArray fileLoadOperationsCount = new SparseIntArray(); + private SparseIntArray audioLoadOperationsCount = new SparseIntArray(); + private SparseIntArray imageLoadOperationsCount = new SparseIntArray(); private ConcurrentHashMap loadOperationPaths = new ConcurrentHashMap<>(); private ArrayList activeFileLoadOperation = new ArrayList<>(); @@ -71,6 +77,8 @@ public class FileLoader extends BaseController { private HashMap loadingVideos = new HashMap<>(); + private String forceLoadingFile; + private static SparseArray mediaDirs = null; private FileLoaderDelegate delegate = null; @@ -355,31 +363,73 @@ public class FileLoader extends BaseController { }); } - private LinkedList getAudioLoadOperationQueue(int datacenterId) { - LinkedList audioLoadOperationQueue = audioLoadOperationQueues.get(datacenterId); - if (audioLoadOperationQueue == null) { - audioLoadOperationQueue = new LinkedList<>(); - audioLoadOperationQueues.put(datacenterId, audioLoadOperationQueue); + private LinkedList getLoadOperationQueue(int datacenterId, int type) { + SparseArray> queues; + if (type == QUEUE_TYPE_AUDIO) { + queues = audioLoadOperationQueues; + } else if (type == QUEUE_TYPE_IMAGE) { + queues = imageLoadOperationQueues; + } else { + queues = fileLoadOperationQueues; } - return audioLoadOperationQueue; + LinkedList queue = queues.get(datacenterId); + if (queue == null) { + queue = new LinkedList<>(); + queues.put(datacenterId, queue); + } + return queue; } - private LinkedList getPhotoLoadOperationQueue(int datacenterId) { - LinkedList photoLoadOperationQueue = photoLoadOperationQueues.get(datacenterId); - if (photoLoadOperationQueue == null) { - photoLoadOperationQueue = new LinkedList<>(); - photoLoadOperationQueues.put(datacenterId, photoLoadOperationQueue); + private SparseIntArray getLoadOperationCount(int type) { + SparseArray> queues; + if (type == QUEUE_TYPE_AUDIO) { + return audioLoadOperationsCount; + } else if (type == QUEUE_TYPE_IMAGE) { + return imageLoadOperationsCount; + } else { + return fileLoadOperationsCount; } - return photoLoadOperationQueue; } - private LinkedList getLoadOperationQueue(int datacenterId) { - LinkedList loadOperationQueue = loadOperationQueues.get(datacenterId); - if (loadOperationQueue == null) { - loadOperationQueue = new LinkedList<>(); - loadOperationQueues.put(datacenterId, loadOperationQueue); + public void setForceStreamLoadingFile(TLRPC.FileLocation location, String ext) { + if (location == null) { + return; } - return loadOperationQueue; + fileLoaderQueue.postRunnable(() -> { + forceLoadingFile = getAttachFileName(location, ext); + FileLoadOperation operation = loadOperationPaths.get(forceLoadingFile); + if (operation != null) { + if (operation.isPreloadVideoOperation()) { + operation.setIsPreloadVideoOperation(false); + } + operation.setForceRequest(true); + int datacenterId = operation.getDatacenterId(); + int queueType = operation.getQueueType(); + LinkedList downloadQueue = getLoadOperationQueue(datacenterId, queueType); + SparseIntArray count = getLoadOperationCount(queueType); + if (downloadQueue != null) { + int index = downloadQueue.indexOf(operation); + if (index >= 0) { + downloadQueue.remove(index); + if (operation.start()) { + count.put(datacenterId, count.get(datacenterId) + 1); + } + if (queueType == QUEUE_TYPE_FILE) { + if (operation.wasStarted() && !activeFileLoadOperation.contains(operation)) { + pauseCurrentFileLoadOperations(operation); + activeFileLoadOperation.add(operation); + } + } + } else { + pauseCurrentFileLoadOperations(operation); + operation.start(); + if (queueType == QUEUE_TYPE_FILE && !activeFileLoadOperation.contains(operation)) { + activeFileLoadOperation.add(operation); + } + } + } + } + }); } public void cancelLoadFile(TLRPC.Document document) { @@ -425,22 +475,14 @@ public class FileLoader extends BaseController { fileLoaderQueue.postRunnable(() -> { FileLoadOperation operation = loadOperationPaths.remove(fileName); if (operation != null) { + int queueType = operation.getQueueType(); int datacenterId = operation.getDatacenterId(); - if (MessageObject.isVoiceDocument(document) || MessageObject.isVoiceWebDocument(webDocument)) { - LinkedList audioLoadOperationQueue = getAudioLoadOperationQueue(datacenterId); - if (!audioLoadOperationQueue.remove(operation)) { - currentAudioLoadOperationsCount.put(datacenterId, currentAudioLoadOperationsCount.get(datacenterId) - 1); - } - } else if (secureDocument != null || location != null || MessageObject.isImageWebDocument(webDocument)) { - LinkedList photoLoadOperationQueue = getPhotoLoadOperationQueue(datacenterId); - if (!photoLoadOperationQueue.remove(operation)) { - currentPhotoLoadOperationsCount.put(datacenterId, currentPhotoLoadOperationsCount.get(datacenterId) - 1); - } - } else { - LinkedList loadOperationQueue = getLoadOperationQueue(datacenterId); - if (!loadOperationQueue.remove(operation)) { - currentLoadOperationsCount.put(datacenterId, currentLoadOperationsCount.get(datacenterId) - 1); - } + LinkedList queue = getLoadOperationQueue(datacenterId, queueType); + if (!queue.remove(operation)) { + SparseIntArray count = getLoadOperationCount(queueType); + count.put(datacenterId, count.get(datacenterId) - 1); + } + if (queueType == QUEUE_TYPE_FILE) { activeFileLoadOperation.remove(operation); } operation.cancel(); @@ -498,23 +540,25 @@ public class FileLoader extends BaseController { private void pauseCurrentFileLoadOperations(FileLoadOperation newOperation) { for (int a = 0; a < activeFileLoadOperation.size(); a++) { FileLoadOperation operation = activeFileLoadOperation.get(a); - if (operation == newOperation || operation.getDatacenterId() != newOperation.getDatacenterId()) { + if (operation == newOperation || operation.getDatacenterId() != newOperation.getDatacenterId() || operation.getFileName().equals(forceLoadingFile)) { continue; } activeFileLoadOperation.remove(operation); a--; int datacenterId = operation.getDatacenterId(); - LinkedList loadOperationQueue = getLoadOperationQueue(datacenterId); - loadOperationQueue.add(0, operation); + int queueType = operation.getQueueType(); + LinkedList downloadQueue = getLoadOperationQueue(datacenterId, queueType); + SparseIntArray count = getLoadOperationCount(queueType); + downloadQueue.add(0, operation); if (operation.wasStarted()) { - currentLoadOperationsCount.put(datacenterId, currentLoadOperationsCount.get(datacenterId) - 1); + count.put(datacenterId, count.get(datacenterId) - 1); } operation.pause(); } } private FileLoadOperation loadFileInternal(final TLRPC.Document document, final SecureDocument secureDocument, final WebFile webDocument, TLRPC.TL_fileLocationToBeDeprecated location, final ImageLocation imageLocation, Object parentObject, final String locationExt, final int locationSize, final int priority, final FileLoadOperationStream stream, final int streamOffset, boolean streamPriority, final int cacheType) { - String fileName = null; + String fileName; if (location != null) { fileName = getAttachFileName(location, locationExt); } else if (secureDocument != null) { @@ -523,6 +567,8 @@ public class FileLoader extends BaseController { fileName = getAttachFileName(document); } else if (webDocument != null) { fileName = getAttachFileName(webDocument); + } else { + fileName = null; } if (fileName == null || fileName.contains("" + Integer.MIN_VALUE)) { return null; @@ -531,45 +577,27 @@ public class FileLoader extends BaseController { loadOperationPathsUI.put(fileName, true); } - FileLoadOperation operation; - operation = loadOperationPaths.get(fileName); + FileLoadOperation operation = loadOperationPaths.get(fileName); if (operation != null) { if (cacheType != 10 && operation.isPreloadVideoOperation()) { operation.setIsPreloadVideoOperation(false); } if (stream != null || priority > 0) { int datacenterId = operation.getDatacenterId(); - - LinkedList audioLoadOperationQueue = getAudioLoadOperationQueue(datacenterId); - LinkedList photoLoadOperationQueue = getPhotoLoadOperationQueue(datacenterId); - LinkedList loadOperationQueue = getLoadOperationQueue(datacenterId); - operation.setForceRequest(true); - LinkedList downloadQueue; - if (MessageObject.isVoiceDocument(document) || MessageObject.isVoiceWebDocument(webDocument)) { - downloadQueue = audioLoadOperationQueue; - } else if (secureDocument != null || location != null || MessageObject.isImageWebDocument(webDocument)) { - downloadQueue = photoLoadOperationQueue; - } else { - downloadQueue = loadOperationQueue; - } + + int queueType = operation.getQueueType(); + LinkedList downloadQueue = getLoadOperationQueue(datacenterId, queueType); + SparseIntArray count = getLoadOperationCount(queueType); if (downloadQueue != null) { int index = downloadQueue.indexOf(operation); if (index >= 0) { downloadQueue.remove(index); if (stream != null) { - if (downloadQueue == audioLoadOperationQueue) { - if (operation.start(stream, streamOffset, streamPriority)) { - currentAudioLoadOperationsCount.put(datacenterId, currentAudioLoadOperationsCount.get(datacenterId) + 1); - } - } else if (downloadQueue == photoLoadOperationQueue) { - if (operation.start(stream, streamOffset, streamPriority)) { - currentPhotoLoadOperationsCount.put(datacenterId, currentPhotoLoadOperationsCount.get(datacenterId) + 1); - } - } else { - if (operation.start(stream, streamOffset, streamPriority)) { - currentLoadOperationsCount.put(datacenterId, currentLoadOperationsCount.get(datacenterId) + 1); - } + if (operation.start(stream, streamOffset, streamPriority)) { + count.put(datacenterId, count.get(datacenterId) + 1); + } + if (queueType == QUEUE_TYPE_FILE) { if (operation.wasStarted() && !activeFileLoadOperation.contains(operation)) { if (stream != null) { pauseCurrentFileLoadOperations(operation); @@ -585,7 +613,7 @@ public class FileLoader extends BaseController { pauseCurrentFileLoadOperations(operation); } operation.start(stream, streamOffset, streamPriority); - if (downloadQueue == loadOperationQueue && !activeFileLoadOperation.contains(operation)) { + if (queueType == QUEUE_TYPE_FILE && !activeFileLoadOperation.contains(operation)) { activeFileLoadOperation.add(operation); } } @@ -598,6 +626,14 @@ public class FileLoader extends BaseController { File tempDir = getDirectory(MEDIA_DIR_CACHE); File storeDir = tempDir; int type = MEDIA_DIR_CACHE; + int queueType; + if (type == MEDIA_DIR_AUDIO) { + queueType = QUEUE_TYPE_AUDIO; + } else if (secureDocument != null || location != null && (imageLocation == null || imageLocation.imageType != IMAGE_TYPE_ANIMATION) || MessageObject.isImageWebDocument(webDocument)) { + queueType = QUEUE_TYPE_IMAGE; + } else { + queueType = QUEUE_TYPE_FILE; + } if (secureDocument != null) { operation = new FileLoadOperation(secureDocument); @@ -606,7 +642,7 @@ public class FileLoader extends BaseController { operation = new FileLoadOperation(imageLocation, parentObject, locationExt, locationSize); type = MEDIA_DIR_IMAGE; } else if (document != null) { - operation = new FileLoadOperation(document, parentObject, fileName); + operation = new FileLoadOperation(document, parentObject); if (MessageObject.isVoiceDocument(document)) { type = MEDIA_DIR_AUDIO; } else if (MessageObject.isVideoDocument(document)) { @@ -631,12 +667,11 @@ public class FileLoader extends BaseController { } else if (cacheType == 2) { operation.setEncryptFile(true); } - operation.setPaths(currentAccount, storeDir, tempDir); + operation.setPaths(currentAccount, fileName, queueType, storeDir, tempDir); if (cacheType == 10) { operation.setIsPreloadVideoOperation(true); } - final String finalFileName = fileName; final int finalType = type; FileLoadOperation.FileLoadOperationDelegate fileLoadOperationDelegate = new FileLoadOperation.FileLoadOperationDelegate() { @Override @@ -645,27 +680,27 @@ public class FileLoader extends BaseController { return; } if (!operation.isPreloadVideoOperation()) { - loadOperationPathsUI.remove(finalFileName); + loadOperationPathsUI.remove(fileName); if (delegate != null) { - delegate.fileDidLoaded(finalFileName, finalFile, finalType); + delegate.fileDidLoaded(fileName, finalFile, finalType); } } - checkDownloadQueue(operation.getDatacenterId(), document, webDocument, location, finalFileName); + checkDownloadQueue(operation.getDatacenterId(), queueType, fileName); } @Override public void didFailedLoadingFile(FileLoadOperation operation, int reason) { - loadOperationPathsUI.remove(finalFileName); - checkDownloadQueue(operation.getDatacenterId(), document, webDocument, location, finalFileName); + loadOperationPathsUI.remove(fileName); + checkDownloadQueue(operation.getDatacenterId(), queueType, fileName); if (delegate != null) { - delegate.fileDidFailedLoad(finalFileName, reason); + delegate.fileDidFailedLoad(fileName, reason); } } @Override public void didChangedLoadProgress(FileLoadOperation operation, long uploadedSize, long totalSize) { if (delegate != null) { - delegate.fileLoadProgressChanged(finalFileName, uploadedSize, totalSize); + delegate.fileLoadProgressChanged(fileName, uploadedSize, totalSize); } } }; @@ -673,47 +708,42 @@ public class FileLoader extends BaseController { int datacenterId = operation.getDatacenterId(); - LinkedList audioLoadOperationQueue = getAudioLoadOperationQueue(datacenterId); - LinkedList photoLoadOperationQueue = getPhotoLoadOperationQueue(datacenterId); - LinkedList loadOperationQueue = getLoadOperationQueue(datacenterId); - loadOperationPaths.put(fileName, operation); operation.setPriority(priority); - if (type == MEDIA_DIR_AUDIO) { + + boolean started; + if (queueType == QUEUE_TYPE_AUDIO) { int maxCount = priority > 0 ? 3 : 1; - int count = currentAudioLoadOperationsCount.get(datacenterId); - if (stream != null || count < maxCount) { + int count = audioLoadOperationsCount.get(datacenterId); + if (started = (stream != null || count < maxCount)) { if (operation.start(stream, streamOffset, streamPriority)) { - currentAudioLoadOperationsCount.put(datacenterId, count + 1); + audioLoadOperationsCount.put(datacenterId, count + 1); } - } else { - addOperationToQueue(operation, audioLoadOperationQueue); } - } else if (location != null || MessageObject.isImageWebDocument(webDocument)) { + } else if (queueType == QUEUE_TYPE_IMAGE) { int maxCount = priority > 0 ? 6 : 2; - int count = currentPhotoLoadOperationsCount.get(datacenterId); - if (stream != null || count < maxCount) { + int count = imageLoadOperationsCount.get(datacenterId); + if (started = (stream != null || count < maxCount)) { if (operation.start(stream, streamOffset, streamPriority)) { - currentPhotoLoadOperationsCount.put(datacenterId, count + 1); + imageLoadOperationsCount.put(datacenterId, count + 1); } - } else { - addOperationToQueue(operation, photoLoadOperationQueue); } } else { int maxCount = priority > 0 ? 4 : 1; - int count = currentLoadOperationsCount.get(datacenterId); - if (stream != null || count < maxCount) { + int count = fileLoadOperationsCount.get(datacenterId); + if (started = (stream != null || count < maxCount)) { if (operation.start(stream, streamOffset, streamPriority)) { - currentLoadOperationsCount.put(datacenterId, count + 1); + fileLoadOperationsCount.put(datacenterId, count + 1); activeFileLoadOperation.add(operation); } if (operation.wasStarted() && stream != null) { pauseCurrentFileLoadOperations(operation); } - } else { - addOperationToQueue(operation, loadOperationQueue); } } + if (!started) { + addOperationToQueue(operation, getLoadOperationQueue(datacenterId, queueType)); + } return operation; } @@ -751,11 +781,11 @@ public class FileLoader extends BaseController { fileLoaderQueue.postRunnable(() -> loadFileInternal(document, secureDocument, webDocument, location, imageLocation, parentObject, locationExt, locationSize, priority, null, 0, false, cacheType)); } - protected FileLoadOperation loadStreamFile(final FileLoadOperationStream stream, final TLRPC.Document document, final Object parentObject, final int offset, final boolean priority) { + protected FileLoadOperation loadStreamFile(final FileLoadOperationStream stream, final TLRPC.Document document, final ImageLocation location, final Object parentObject, final int offset, final boolean priority) { final CountDownLatch semaphore = new CountDownLatch(1); final FileLoadOperation[] result = new FileLoadOperation[1]; fileLoaderQueue.postRunnable(() -> { - result[0] = loadFileInternal(document, null, null, null, null, parentObject, null, 0, 1, stream, offset, priority, 0); + result[0] = loadFileInternal(document, null, null, document == null && location != null ? location.location : null, location, parentObject, document == null && location != null ? "mp4" : null, document == null && location != null ? location.currentSize : 0, 1, stream, offset, priority, document == null ? 1 : 0); semaphore.countDown(); }); try { @@ -766,92 +796,53 @@ public class FileLoader extends BaseController { return result[0]; } - private void checkDownloadQueue(final int datacenterId, final TLRPC.Document document, final WebFile webDocument, final TLRPC.FileLocation location, final String arg1) { + private void checkDownloadQueue(int datacenterId, int queueType, String fileName) { fileLoaderQueue.postRunnable(() -> { - LinkedList audioLoadOperationQueue = getAudioLoadOperationQueue(datacenterId); - LinkedList photoLoadOperationQueue = getPhotoLoadOperationQueue(datacenterId); - LinkedList loadOperationQueue = getLoadOperationQueue(datacenterId); - - FileLoadOperation operation = loadOperationPaths.remove(arg1); - if (MessageObject.isVoiceDocument(document) || MessageObject.isVoiceWebDocument(webDocument)) { - int count = currentAudioLoadOperationsCount.get(datacenterId); - if (operation != null) { - if (operation.wasStarted()) { - count--; - currentAudioLoadOperationsCount.put(datacenterId, count); - } else { - audioLoadOperationQueue.remove(operation); - } + FileLoadOperation operation = loadOperationPaths.remove(fileName); + LinkedList queue = getLoadOperationQueue(datacenterId, queueType); + SparseIntArray operationCount = getLoadOperationCount(queueType); + int count = operationCount.get(datacenterId); + if (operation != null) { + if (operation.wasStarted()) { + count--; + operationCount.put(datacenterId, count); + } else { + queue.remove(operation); } - while (!audioLoadOperationQueue.isEmpty()) { - operation = audioLoadOperationQueue.get(0); - int maxCount = operation.getPriority() != 0 ? 3 : 1; - if (count < maxCount) { - operation = audioLoadOperationQueue.poll(); - if (operation != null && operation.start()) { - count++; - currentAudioLoadOperationsCount.put(datacenterId, count); - } - } else { - break; - } - } - } else if (location != null || MessageObject.isImageWebDocument(webDocument)) { - int count = currentPhotoLoadOperationsCount.get(datacenterId); - if (operation != null) { - if (operation.wasStarted()) { - count--; - currentPhotoLoadOperationsCount.put(datacenterId, count); - } else { - photoLoadOperationQueue.remove(operation); - } - } - while (!photoLoadOperationQueue.isEmpty()) { - operation = photoLoadOperationQueue.get(0); - int maxCount = operation.getPriority() != 0 ? 6 : 2; - if (count < maxCount) { - operation = photoLoadOperationQueue.poll(); - if (operation != null && operation.start()) { - count++; - currentPhotoLoadOperationsCount.put(datacenterId, count); - } - } else { - break; - } - } - } else { - int count = currentLoadOperationsCount.get(datacenterId); - if (operation != null) { - if (operation.wasStarted()) { - count--; - currentLoadOperationsCount.put(datacenterId, count); - } else { - loadOperationQueue.remove(operation); - } + if (queueType == QUEUE_TYPE_FILE) { activeFileLoadOperation.remove(operation); } - while (!loadOperationQueue.isEmpty()) { - operation = loadOperationQueue.get(0); - int maxCount = operation.isForceRequest() ? 3 : 1; - if (count < maxCount) { - operation = loadOperationQueue.poll(); - if (operation != null && operation.start()) { - count++; - currentLoadOperationsCount.put(datacenterId, count); + } + while (!queue.isEmpty()) { + operation = queue.get(0); + int maxCount; + if (queueType == QUEUE_TYPE_AUDIO) { + maxCount = operation.getPriority() != 0 ? 3 : 1; + } else if (queueType == QUEUE_TYPE_IMAGE) { + maxCount = operation.getPriority() != 0 ? 6 : 2; + } else { + maxCount = operation.isForceRequest() ? 3 : 1; + } + if (count < maxCount) { + operation = queue.poll(); + if (operation != null && operation.start()) { + count++; + operationCount.put(datacenterId, count); + if (queueType == QUEUE_TYPE_FILE) { if (!activeFileLoadOperation.contains(operation)) { activeFileLoadOperation.add(operation); } } - } else { - break; } + } else { + break; } } }); } - public void setDelegate(FileLoaderDelegate delegate) { - this.delegate = delegate; + public void setDelegate(FileLoaderDelegate fileLoaderDelegate) { + delegate = fileLoaderDelegate; } public static String getMessageFileName(TLRPC.Message message) { @@ -985,6 +976,13 @@ public class FileLoader extends BaseController { } else { dir = getDirectory(MEDIA_DIR_IMAGE); } + } else if (attach instanceof TLRPC.TL_videoSize) { + TLRPC.TL_videoSize videoSize = (TLRPC.TL_videoSize) attach; + if (videoSize.location == null || videoSize.location.key != null || videoSize.location.volume_id == Integer.MIN_VALUE && videoSize.location.local_id < 0 || videoSize.size < 0) { + dir = getDirectory(MEDIA_DIR_CACHE); + } else { + dir = getDirectory(MEDIA_DIR_IMAGE); + } } else if (attach instanceof TLRPC.FileLocation) { TLRPC.FileLocation fileLocation = (TLRPC.FileLocation) attach; if (fileLocation.key != null || fileLocation.volume_id == Integer.MIN_VALUE && fileLocation.local_id < 0) { @@ -1167,6 +1165,12 @@ public class FileLoader extends BaseController { return ""; } return photo.location.volume_id + "_" + photo.location.local_id + "." + (ext != null ? ext : "jpg"); + } else if (attach instanceof TLRPC.TL_videoSize) { + TLRPC.TL_videoSize video = (TLRPC.TL_videoSize) attach; + if (video.location == null || video.location instanceof TLRPC.TL_fileLocationUnavailable) { + return ""; + } + return video.location.volume_id + "_" + video.location.local_id + "." + (ext != null ? ext : "mp4"); } else if (attach instanceof TLRPC.FileLocation) { if (attach instanceof TLRPC.TL_fileLocationUnavailable) { return ""; @@ -1252,4 +1256,17 @@ public class FileLoader extends BaseController { out.close(); return true; } + + public static boolean isSamePhoto(TLRPC.FileLocation location, TLRPC.Photo photo) { + if (location == null || !(photo instanceof TLRPC.TL_photo)) { + return false; + } + for (int b = 0, N = photo.sizes.size(); b < N; b++) { + TLRPC.PhotoSize size = photo.sizes.get(b); + if (size.location != null && size.location.local_id == location.local_id && size.location.volume_id == location.volume_id) { + return true; + } + } + return false; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/FileRefController.java b/TMessagesProj/src/main/java/org/telegram/messenger/FileRefController.java index 69be9cfd5..e179ca81c 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/FileRefController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/FileRefController.java @@ -75,7 +75,7 @@ public class FileRefController extends BaseController { } else if (parentObject instanceof TLRPC.Message) { TLRPC.Message message = (TLRPC.Message) parentObject; int channelId = message.to_id != null ? message.to_id.channel_id : 0; - return "message" + message.id + "_" + channelId; + return "message" + message.id + "_" + channelId + "_" + message.from_scheduled; } else if (parentObject instanceof TLRPC.WebPage) { TLRPC.WebPage webPage = (TLRPC.WebPage) parentObject; return "webpage" + webPage.id; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/FileStreamLoadOperation.java b/TMessagesProj/src/main/java/org/telegram/messenger/FileStreamLoadOperation.java index 67ee8a0b5..55ab9f99f 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/FileStreamLoadOperation.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/FileStreamLoadOperation.java @@ -69,7 +69,7 @@ public class FileStreamLoadOperation extends BaseDataSource implements FileLoadO } else if (document.mime_type.startsWith("audio")) { document.attributes.add(new TLRPC.TL_documentAttributeAudio()); } - loadOperation = FileLoader.getInstance(currentAccount).loadStreamFile(this, document, parentObject, currentOffset = (int) dataSpec.position, false); + loadOperation = FileLoader.getInstance(currentAccount).loadStreamFile(this, document, null, parentObject, currentOffset = (int) dataSpec.position, false); bytesRemaining = dataSpec.length == C.LENGTH_UNSET ? document.size - dataSpec.position : dataSpec.length; if (bytesRemaining < 0) { throw new EOFException(); @@ -98,7 +98,7 @@ public class FileStreamLoadOperation extends BaseDataSource implements FileLoadO while (availableLength == 0 && opened) { availableLength = loadOperation.getDownloadedLengthFromOffset(currentOffset, readLength)[0]; if (availableLength == 0) { - FileLoader.getInstance(currentAccount).loadStreamFile(this, document, parentObject, currentOffset, false); + FileLoader.getInstance(currentAccount).loadStreamFile(this, document, null, parentObject, currentOffset, false); countDownLatch = new CountDownLatch(1); countDownLatch.await(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/FileUploadOperation.java b/TMessagesProj/src/main/java/org/telegram/messenger/FileUploadOperation.java index 8c5c21e61..df165f59b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/FileUploadOperation.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/FileUploadOperation.java @@ -40,6 +40,7 @@ public class FileUploadOperation { private static final int initialRequestsSlowNetworkCount = 1; private static final int maxUploadingKBytes = 1024 * 2; private static final int maxUploadingSlowNetworkKBytes = 32; + private static final int maxUploadParts = (int) (FileLoader.MAX_FILE_SIZE / 1024 / 512); private int maxRequestsCount; private int uploadChunkSize = 64 * 1024; private boolean slowNetwork; @@ -256,7 +257,7 @@ public class FileUploadOperation { isBigFile = true; } - uploadChunkSize = (int) Math.max(slowNetwork ? minUploadChunkSlowNetworkSize : minUploadChunkSize, (totalFileSize + 1024 * 3000 - 1) / (1024 * 3000)); + uploadChunkSize = (int) Math.max(slowNetwork ? minUploadChunkSlowNetworkSize : minUploadChunkSize, (totalFileSize + 1024 * maxUploadParts - 1) / (1024 * maxUploadParts)); if (1024 % uploadChunkSize != 0) { int chunkSize = 64; while (uploadChunkSize > chunkSize) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java index 8b81b0e95..ab85ee4df 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java @@ -164,7 +164,6 @@ public class ImageLoader { URL downloadUrl = new URL(url); httpConnection = downloadUrl.openConnection(); httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A5297c Safari/602.1"); - //httpConnection.addRequestProperty("Referer", "google.com"); httpConnection.setConnectTimeout(5000); httpConnection.setReadTimeout(5000); if (httpConnection instanceof HttpURLConnection) { @@ -178,7 +177,6 @@ public class ImageLoader { httpConnection = downloadUrl.openConnection(); httpConnection.setRequestProperty("Cookie", cookies); httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A5297c Safari/602.1"); - //httpConnection.addRequestProperty("Referer", "google.com"); } } httpConnection.connect(); @@ -318,7 +316,7 @@ public class ImageLoader { String location = cacheImage.imageLocation.path; URL downloadUrl = new URL(location.replace("athumb://", "https://")); httpConnection = (HttpURLConnection) downloadUrl.openConnection(); - httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A5297c Safari/602.1"); + //httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A5297c Safari/602.1"); httpConnection.setConnectTimeout(5000); httpConnection.setReadTimeout(5000); httpConnection.connect(); @@ -479,7 +477,6 @@ public class ImageLoader { URL downloadUrl = new URL(overrideUrl != null ? overrideUrl : location); httpConnection = (HttpURLConnection) downloadUrl.openConnection(); httpConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A5297c Safari/602.1"); - //httpConnection.addRequestProperty("Referer", "google.com"); httpConnection.setConnectTimeout(5000); httpConnection.setReadTimeout(5000); httpConnection.setInstanceFollowRedirects(true); @@ -804,7 +801,7 @@ public class ImageLoader { h = Math.min(h, 160); limitFps = true; } - precache = SharedConfig.getDevicePerfomanceClass() != SharedConfig.PERFORMANCE_CLASS_HIGH; + precache = SharedConfig.getDevicePerformanceClass() != SharedConfig.PERFORMANCE_CLASS_HIGH; } if (args.length >= 3) { @@ -841,10 +838,18 @@ public class ImageLoader { onPostExecute(lottieDrawable); } else if (cacheImage.imageType == FileLoader.IMAGE_TYPE_ANIMATION) { AnimatedFileDrawable fileDrawable; - if (AUTOPLAY_FILTER.equals(cacheImage.filter) && !(cacheImage.imageLocation.document instanceof TLRPC.TL_documentEncrypted)) { - fileDrawable = new AnimatedFileDrawable(cacheImage.finalFilePath, false, cacheImage.size, cacheImage.imageLocation.document instanceof TLRPC.Document ? cacheImage.imageLocation.document : null, cacheImage.parentObject, cacheImage.currentAccount, false); + long seekTo; + if (cacheImage.imageLocation != null) { + seekTo = cacheImage.imageLocation.videoSeekTo; } else { - fileDrawable = new AnimatedFileDrawable(cacheImage.finalFilePath, "d".equals(cacheImage.filter), 0, null, null, cacheImage.currentAccount, false); + seekTo = 0; + } + if (AUTOPLAY_FILTER.equals(cacheImage.filter) && !(cacheImage.imageLocation.document instanceof TLRPC.TL_documentEncrypted)) { + TLRPC.Document document = cacheImage.imageLocation.document instanceof TLRPC.Document ? cacheImage.imageLocation.document : null; + int size = document != null ? cacheImage.size : cacheImage.imageLocation.currentSize; + fileDrawable = new AnimatedFileDrawable(cacheImage.finalFilePath, false, size, document, document == null ? cacheImage.imageLocation : null, cacheImage.parentObject, seekTo, cacheImage.currentAccount, false); + } else { + fileDrawable = new AnimatedFileDrawable(cacheImage.finalFilePath, "d".equals(cacheImage.filter), 0, null, null, null, seekTo, cacheImage.currentAccount, false); } Thread.interrupted(); onPostExecute(fileDrawable); @@ -2373,7 +2378,14 @@ public class ImageLoader { } else if (imageLocation.webFile != null) { cacheFile = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_DOCUMENT), url); } else { - cacheFile = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_IMAGE), url); + if (cacheType == 1) { + cacheFile = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), url); + } else { + cacheFile = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_IMAGE), url); + } + if (AUTOPLAY_FILTER.equals(filter) && imageLocation.location != null && !cacheFile.exists()) { + cacheFile = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), imageLocation.location.volume_id + "_" + imageLocation.location.local_id + ".temp"); + } } if (AUTOPLAY_FILTER.equals(filter)) { img.imageType = FileLoader.IMAGE_TYPE_ANIMATION; @@ -2527,8 +2539,9 @@ public class ImageLoader { String thumbFilter = imageReceiver.getThumbFilter(); ImageLocation mediaLocation = imageReceiver.getMediaLocation(); String mediaFilter = imageReceiver.getMediaFilter(); - ImageLocation imageLocation = imageReceiver.getImageLocation(); + ImageLocation originalImageLocation = imageReceiver.getImageLocation(); String imageFilter = imageReceiver.getImageFilter(); + ImageLocation imageLocation = originalImageLocation; if (imageLocation == null && imageReceiver.isNeedsQualityThumb() && imageReceiver.isCurrentKeyQuality()) { if (parentObject instanceof MessageObject) { MessageObject messageObject = (MessageObject) parentObject; @@ -2648,7 +2661,7 @@ public class ImageLoader { if (thumbLocation != null) { ImageLocation strippedLoc = imageReceiver.getStrippedLocation(); if (strippedLoc == null) { - strippedLoc = mediaLocation != null ? mediaLocation : imageLocation; + strippedLoc = mediaLocation != null ? mediaLocation : originalImageLocation; } thumbKey = thumbLocation.getKey(parentObject, strippedLoc, false); thumbUrl = thumbLocation.getKey(parentObject, strippedLoc, true); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLocation.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLocation.java index b5ff18e14..46309578d 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLocation.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLocation.java @@ -19,6 +19,8 @@ public class ImageLocation { public TLRPC.Document document; + public long videoSeekTo; + public TLRPC.PhotoSize photoSize; public TLRPC.Photo photo; public boolean photoPeerBig; @@ -176,7 +178,7 @@ public class ImageLocation { return imageLocation; } - public static ImageLocation getForDocument(TLRPC.TL_videoSize videoSize, TLRPC.Document document) { + public static ImageLocation getForDocument(TLRPC.VideoSize videoSize, TLRPC.Document document) { if (videoSize == null || document == null) { return null; } @@ -185,6 +187,18 @@ public class ImageLocation { return location; } + public static ImageLocation getForPhoto(TLRPC.VideoSize videoSize, TLRPC.Photo photo) { + if (videoSize == null || photo == null) { + return null; + } + ImageLocation location = getForPhoto(videoSize.location, videoSize.size, photo, null, null, false, photo.dc_id, null, videoSize.type); + location.imageType = FileLoader.IMAGE_TYPE_ANIMATION; + if ((videoSize.flags & 1) != 0) { + location.videoSeekTo = (int) (videoSize.video_start_ts * 1000); + } + return location; + } + public static ImageLocation getForDocument(TLRPC.PhotoSize photoSize, TLRPC.Document document) { if (photoSize instanceof TLRPC.TL_photoStrippedSize) { ImageLocation imageLocation = new ImageLocation(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java index 3c5a46104..d5c5813c1 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java @@ -43,10 +43,22 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg private String key; private boolean recycleOnRelease; public Bitmap bitmap; + public Drawable drawable; + public int orientation; - public BitmapHolder(Bitmap b, String k) { + public BitmapHolder(Bitmap b, String k, int o) { bitmap = b; key = k; + orientation = o; + if (key != null) { + ImageLoader.getInstance().incrementUseCount(key); + } + } + + public BitmapHolder(Drawable d, String k, int o) { + drawable = d; + key = k; + orientation = o; if (key != null) { ImageLoader.getInstance().incrementUseCount(key); } @@ -75,16 +87,31 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg bitmap.recycle(); } bitmap = null; + drawable = null; return; } boolean canDelete = ImageLoader.getInstance().decrementUseCount(key); if (!ImageLoader.getInstance().isInMemCache(key, false)) { if (canDelete) { - bitmap.recycle(); + if (bitmap != null) { + bitmap.recycle(); + } else if (drawable != null) { + if (drawable instanceof RLottieDrawable) { + RLottieDrawable fileDrawable = (RLottieDrawable) drawable; + fileDrawable.recycle(); + } else if (drawable instanceof AnimatedFileDrawable) { + AnimatedFileDrawable fileDrawable = (AnimatedFileDrawable) drawable; + fileDrawable.recycle(); + } else if (drawable instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); + bitmap.recycle(); + } + } } } key = null; bitmap = null; + drawable = null; } } @@ -903,7 +930,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg roundPaint.setAlpha(alpha); if (isRoundRect) { - canvas.drawRoundRect(roundRect,roundRadius[0], roundRadius[0],roundPaint); + canvas.drawRoundRect(roundRect, roundRadius[0], roundRadius[0], roundPaint); } else { for (int a = 0; a < roundRadius.length; a++) { radii[a * 2] = roundRadius[a]; @@ -1083,6 +1110,9 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg AnimatedFileDrawable animation = getAnimation(); RLottieDrawable lottieDrawable = getLottieAnimation(); boolean animationNotReady = animation != null && !animation.hasBitmap() || lottieDrawable != null && !lottieDrawable.hasBitmap(); + if (animation != null) { + animation.setRoundRadius(roundRadius); + } if (lottieDrawable != null) { lottieDrawable.setCurrentParentView(parentView); } @@ -1222,10 +1252,15 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg String key = null; AnimatedFileDrawable animation = getAnimation(); RLottieDrawable lottieDrawable = getLottieAnimation(); + int orientation = 0; if (lottieDrawable != null && lottieDrawable.hasBitmap()) { bitmap = lottieDrawable.getAnimatedBitmap(); } else if (animation != null && animation.hasBitmap()) { bitmap = animation.getAnimatedBitmap(); + orientation = animation.getOrientation(); + if (orientation != 0) { + return new BitmapHolder(Bitmap.createBitmap(bitmap), null, orientation); + } } else if (currentMediaDrawable instanceof BitmapDrawable && !(currentMediaDrawable instanceof AnimatedFileDrawable) && !(currentMediaDrawable instanceof RLottieDrawable)) { bitmap = ((BitmapDrawable) currentMediaDrawable).getBitmap(); key = currentMediaKey; @@ -1239,7 +1274,28 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg bitmap = ((BitmapDrawable) staticThumbDrawable).getBitmap(); } if (bitmap != null) { - return new BitmapHolder(bitmap, key); + return new BitmapHolder(bitmap, key, orientation); + } + return null; + } + + public BitmapHolder getDrawableSafe() { + Drawable drawable = null; + String key = null; + if (currentMediaDrawable instanceof BitmapDrawable && !(currentMediaDrawable instanceof AnimatedFileDrawable) && !(currentMediaDrawable instanceof RLottieDrawable)) { + drawable = currentMediaDrawable; + key = currentMediaKey; + } else if (currentImageDrawable instanceof BitmapDrawable && !(currentImageDrawable instanceof AnimatedFileDrawable) && !(currentMediaDrawable instanceof RLottieDrawable)) { + drawable = currentImageDrawable; + key = currentImageKey; + } else if (currentThumbDrawable instanceof BitmapDrawable && !(currentThumbDrawable instanceof AnimatedFileDrawable) && !(currentMediaDrawable instanceof RLottieDrawable)) { + drawable = currentThumbDrawable; + key = currentThumbKey; + } else if (staticThumbDrawable instanceof BitmapDrawable) { + drawable = staticThumbDrawable; + } + if (drawable != null) { + return new BitmapHolder(drawable, key, 0); } return null; } @@ -1263,7 +1319,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg bitmap = ((BitmapDrawable) staticThumbDrawable).getBitmap(); } if (bitmap != null) { - return new BitmapHolder(bitmap, key); + return new BitmapHolder(bitmap, key, 0); } return null; } @@ -1593,6 +1649,10 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg allowStartAnimation = value; } + public boolean getAllowStartAnimation() { + return allowStartAnimation; + } + public void setAllowStartLottieAnimation(boolean value) { allowStartLottieAnimation = value; } @@ -1712,7 +1772,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg } updateDrawableRadius(drawable); - if (!memCache && !forcePreview || forceCrossfade) { + if (isVisible && (!memCache && !forcePreview || forceCrossfade)) { boolean allowCorssfade = true; if (currentMediaDrawable instanceof AnimatedFileDrawable && ((AnimatedFileDrawable) currentMediaDrawable).hasBitmap()) { allowCorssfade = false; @@ -1844,7 +1904,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg key = currentImageKey; image = currentImageDrawable; } - if (key != null && key.startsWith("-")) { + if (key != null && (key.startsWith("-") || key.startsWith("strippedmessage-"))) { String replacedKey = ImageLoader.getInstance().getReplacedKey(key); if (replacedKey != null) { key = replacedKey; @@ -1854,7 +1914,6 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg RLottieDrawable lottieDrawable = (RLottieDrawable) image; lottieDrawable.removeParentView(parentView); } - String replacedKey = ImageLoader.getInstance().getReplacedKey(key); if (key != null && (newKey == null || !newKey.equals(key)) && image != null) { if (image instanceof RLottieDrawable) { RLottieDrawable fileDrawable = (RLottieDrawable) image; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java b/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java index 3eb900cf7..aec5412ce 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/LocaleController.java @@ -1298,6 +1298,32 @@ public class LocaleController { } } + public static String formatDuration(int duration) { + if (duration <= 0) { + return formatPluralString("Seconds", 0); + } + final int hours = duration / 3600; + final int minutes = duration / 60 % 60; + final int seconds = duration % 60; + final StringBuilder stringBuilder = new StringBuilder(); + if (hours > 0) { + stringBuilder.append(formatPluralString("Hours", hours)); + } + if (minutes > 0) { + if (stringBuilder.length() > 0) { + stringBuilder.append(' '); + } + stringBuilder.append(formatPluralString("Minutes", minutes)); + } + if (seconds > 0) { + if (stringBuilder.length() > 0) { + stringBuilder.append(' '); + } + stringBuilder.append(formatPluralString("Seconds", seconds)); + } + return stringBuilder.toString(); + } + public static String formatCallDuration(int duration) { if (duration > 3600) { String result = LocaleController.formatPluralString("Hours", duration / 3600); @@ -1733,6 +1759,22 @@ public class LocaleController { return formatUserStatus(currentAccount, user, null); } + public static String formatJoined(long date) { + try { + date *= 1000; + String format; + if (Math.abs(System.currentTimeMillis() - date) < 31536000000L) { + format = LocaleController.formatString("formatDateAtTime", R.string.formatDateAtTime, getInstance().formatterDayMonth.format(new Date(date)), getInstance().formatterDay.format(new Date(date))); + } else { + format = LocaleController.formatString("formatDateAtTime", R.string.formatDateAtTime, getInstance().formatterYear.format(new Date(date)), getInstance().formatterDay.format(new Date(date))); + } + return formatString("ChannelOtherSubscriberJoined", R.string.ChannelOtherSubscriberJoined, format); + } catch (Exception e) { + FileLog.e(e); + } + return "LOC_ERR"; + } + public static String formatUserStatus(int currentAccount, TLRPC.User user, boolean[] isOnline) { if (user != null && user.status != null && user.status.expires == 0) { if (user.status instanceof TLRPC.TL_userStatusRecently) { @@ -2925,7 +2967,7 @@ public class LocaleController { useImperialSystemType = null; } - public static String formatDistance(float distance) { + public static String formatDistance(float distance, int type) { if (useImperialSystemType == null) { if (SharedConfig.distanceSystemType == 0) { try { @@ -2945,7 +2987,13 @@ public class LocaleController { if (useImperialSystemType) { distance *= 3.28084f; if (distance < 1000) { - return formatString("FootsAway", R.string.FootsAway, String.format("%d", (int) Math.max(1, distance))); + switch (type) { + case 0: + return formatString("FootsAway", R.string.FootsAway, String.format("%d", (int) Math.max(1, distance))); + case 1: + default: + return formatString("FootsFromYou", R.string.FootsFromYou, String.format("%d", (int) Math.max(1, distance))); + } } else { String arg; if (distance % 5280 == 0) { @@ -2953,11 +3001,24 @@ public class LocaleController { } else { arg = String.format("%.2f", distance / 5280.0f); } - return formatString("MilesAway", R.string.MilesAway, arg); + switch (type) { + case 0: + return formatString("MilesAway", R.string.MilesAway, arg); + case 1: + default: + return formatString("MilesFromYou", R.string.MilesFromYou, arg); + } + } } else { if (distance < 1000) { - return formatString("MetersAway2", R.string.MetersAway2, String.format("%d", (int) Math.max(1, distance))); + switch (type) { + case 0: + return formatString("MetersAway2", R.string.MetersAway2, String.format("%d", (int) Math.max(1, distance))); + case 1: + default: + return formatString("MetersFromYou2", R.string.MetersFromYou2, String.format("%d", (int) Math.max(1, distance))); + } } else { String arg; if (distance % 1000 == 0) { @@ -2965,7 +3026,13 @@ public class LocaleController { } else { arg = String.format("%.2f", distance / 1000.0f); } - return formatString("KMetersAway2", R.string.KMetersAway2, arg); + switch (type) { + case 0: + return formatString("KMetersAway2", R.string.KMetersAway2, arg); + case 1: + default: + return formatString("KMetersFromYou2", R.string.KMetersFromYou2, arg); + } } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java index 2746a5af0..74d7fb356 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java @@ -20,6 +20,7 @@ import android.content.pm.PackageManager; import android.database.ContentObserver; import android.database.Cursor; import android.graphics.BitmapFactory; +import android.graphics.Matrix; import android.graphics.Point; import android.graphics.SurfaceTexture; import android.hardware.Sensor; @@ -167,6 +168,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public static class SavedFilterState { public float enhanceValue; + public float softenSkinValue; public float exposureValue; public float contrastValue; public float warmthValue; @@ -187,6 +189,29 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public float blurAngle; } + public static class CropState { + public float cropPx; + public float cropPy; + public float cropScale = 1; + public float cropRotate; + public float cropPw = 1; + public float cropPh = 1; + public int transformWidth; + public int transformHeight; + public int transformRotation; + public boolean mirrored; + + public float stateScale; + public float scale; + public Matrix matrix; + public int width; + public int height; + public boolean freeform; + public float lockedAspectRatio; + + public boolean initied; + } + public static class MediaEditState { public CharSequence caption; @@ -195,12 +220,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public String imagePath; public String filterPath; public String paintPath; + public String croppedPaintPath; public String fullPaintPath; - public String croppedPath; public ArrayList entities; public SavedFilterState savedFilterState; public ArrayList mediaEntities; + public ArrayList croppedMediaEntities; public ArrayList stickers; public VideoEditedInfo editedInfo; public long averageDuration; @@ -209,6 +235,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public boolean isCropped; public int ttl; + public CropState cropState; + public String getPath() { return null; } @@ -219,6 +247,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, filterPath = null; imagePath = null; paintPath = null; + croppedPaintPath = null; isFiltered = false; isPainted = false; isCropped = false; @@ -227,8 +256,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, editedInfo = null; entities = null; savedFilterState = null; - croppedPath = null; stickers = null; + cropState = null; } } @@ -419,6 +448,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private boolean isPaused = false; private VideoPlayer audioPlayer = null; + private boolean isStreamingCurrentAudio; private int playerNum; private String shouldSavePositionForCurrentAudio; private long lastSaveTime; @@ -430,10 +460,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private MessageObject goingToShowMessageObject; private Timer progressTimer = null; private final Object progressTimerSync = new Object(); - private ArrayList playlist = new ArrayList<>(); - private ArrayList shuffledPlaylist = new ArrayList<>(); - private int currentPlaylistNum; - private boolean forceLoopCurrentPlaylist; private boolean downloadingCurrentMessage; private boolean playMusicAgain; private AudioInfo audioInfo; @@ -452,6 +478,17 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private float currentAspectRatioFrameLayoutRatio; private boolean currentAspectRatioFrameLayoutReady; + private ArrayList playlist = new ArrayList<>(); + private HashMap playlistMap = new HashMap<>(); + private ArrayList shuffledPlaylist = new ArrayList<>(); + private int currentPlaylistNum; + private boolean forceLoopCurrentPlaylist; + private boolean[] playlistEndReached = new boolean[]{false, false}; + private boolean loadingPlaylist; + private long playlistMergeDialogId; + private int playlistClassGuid; + private int[] playlistMaxId = new int[]{Integer.MAX_VALUE, Integer.MAX_VALUE}; + private Runnable setLoadingRunnable = new Runnable() { @Override public void run() { @@ -845,6 +882,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, NotificationCenter.getInstance(a).addObserver(MediaController.this, NotificationCenter.messagesDeleted); NotificationCenter.getInstance(a).addObserver(MediaController.this, NotificationCenter.removeAllMessagesFromDialog); NotificationCenter.getInstance(a).addObserver(MediaController.this, NotificationCenter.musicDidLoad); + NotificationCenter.getInstance(a).addObserver(MediaController.this, NotificationCenter.mediaDidLoad); NotificationCenter.getGlobalInstance().addObserver(MediaController.this, NotificationCenter.playerDidStartPlaying); } }); @@ -969,11 +1007,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, progress = audioPlayer.getCurrentPosition(); value = duration >= 0 ? (progress / (float) duration) : 0.0f; bufferedValue = audioPlayer.getBufferedPosition() / (float) duration; - /*if (audioPlayer.isStreaming()) { - bufferedValue = FileLoader.getInstance(currentPlayingMessageObject.currentAccount).getBufferedProgressFromPosition(value, fileName); - } else { - bufferedValue = 1.0f; - }*/ if (duration == C.TIME_UNSET || progress < 0 || seekToProgressPending != 0) { return; } @@ -1017,21 +1050,31 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public void cleanup() { - cleanupPlayer(false, true); + cleanupPlayer(true, true); audioInfo = null; playMusicAgain = false; for (int a = 0; a < UserConfig.MAX_ACCOUNT_COUNT; a++) { DownloadController.getInstance(a).cleanup(); } videoConvertQueue.clear(); - playlist.clear(); - shuffledPlaylist.clear(); generatingWaveform.clear(); voiceMessagesPlaylist = null; voiceMessagesPlaylistMap = null; + clearPlaylist(); cancelVideoConvert(null); } + private void clearPlaylist() { + playlist.clear(); + playlistMap.clear(); + shuffledPlaylist.clear(); + playlistClassGuid = 0; + playlistEndReached[0] = playlistEndReached[1] = false; + playlistMergeDialogId = 0; + playlistMaxId[0] = playlistMaxId[1] = Integer.MAX_VALUE; + loadingPlaylist = false; + } + public void startMediaObserver() { ApplicationLoader.applicationHandler.removeCallbacks(stopMediaObserverRunnable); startObserverToken++; @@ -1164,11 +1207,20 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public void didReceivedNotification(int id, int account, Object... args) { if (id == NotificationCenter.fileDidLoad || id == NotificationCenter.httpFileDidLoad) { String fileName = (String) args[0]; - if (downloadingCurrentMessage && playingMessageObject != null && playingMessageObject.currentAccount == account) { + if (playingMessageObject != null && playingMessageObject.currentAccount == account) { String file = FileLoader.getAttachFileName(playingMessageObject.getDocument()); if (file.equals(fileName)) { - playMusicAgain = true; - playMessage(playingMessageObject); + if (downloadingCurrentMessage) { + playMusicAgain = true; + playMessage(playingMessageObject); + } else if (audioInfo == null) { + try { + File cacheFile = FileLoader.getPathToMessage(playingMessageObject.messageOwner); + audioInfo = AudioInfo.getAudioInfo(cacheFile); + } catch (Exception e) { + FileLog.e(e); + } + } } } } else if (id == NotificationCenter.messagesDeleted) { @@ -1206,13 +1258,52 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } else if (id == NotificationCenter.musicDidLoad) { long did = (Long) args[0]; if (playingMessageObject != null && playingMessageObject.isMusic() && playingMessageObject.getDialogId() == did && !playingMessageObject.scheduled) { - ArrayList arrayList = (ArrayList) args[1]; - playlist.addAll(0, arrayList); + ArrayList arrayListBegin = (ArrayList) args[1]; + ArrayList arrayListEnd = (ArrayList) args[2]; + playlist.addAll(0, arrayListBegin); + playlist.addAll(arrayListEnd); + for (int a = 0, N = playlist.size(); a < N; a++) { + MessageObject object = playlist.get(a); + playlistMap.put(object.getId(), object); + playlistMaxId[0] = Math.min(playlistMaxId[0], object.getId()); + } + playlistClassGuid = ConnectionsManager.generateClassGuid(); if (SharedConfig.shuffleMusic) { buildShuffledPlayList(); currentPlaylistNum = 0; } else { - currentPlaylistNum += arrayList.size(); + currentPlaylistNum += arrayListBegin.size(); + } + } + } else if (id == NotificationCenter.mediaDidLoad) { + int guid = (Integer) args[3]; + if (guid == playlistClassGuid && playingMessageObject != null) { + long did = (Long) args[0]; + int type = (Integer) args[4]; + + ArrayList arr = (ArrayList) args[2]; + boolean enc = ((int) did) == 0; + int loadIndex = did == playlistMergeDialogId ? 1 : 0; + if (!arr.isEmpty()) { + playlistEndReached[loadIndex] = (Boolean) args[5]; + } + int addedCount = 0; + for (int a = 0; a < arr.size(); a++) { + MessageObject message = arr.get(a); + if (playlistMap.containsKey(message.getId())) { + continue; + } + addedCount++; + playlist.add(0, message); + playlistMap.put(message.getId(), message); + playlistMaxId[loadIndex] = Math.min(playlistMaxId[loadIndex], message.getId()); + } + loadingPlaylist = false; + if (SharedConfig.shuffleMusic) { + buildShuffledPlayList(); + } + if (addedCount != 0) { + NotificationCenter.getInstance(playingMessageObject.currentAccount).postNotificationName(NotificationCenter.moreMusicDidLoad, addedCount); } } } else if (id == NotificationCenter.didReceiveNewMessages) { @@ -1806,29 +1897,44 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } } - public boolean setPlaylist(ArrayList messageObjects, MessageObject current) { - return setPlaylist(messageObjects, current, true); + public void loadMoreMusic() { + if (loadingPlaylist || playingMessageObject == null || playingMessageObject.scheduled || (int) playingMessageObject.getDialogId() == 0 || playlistClassGuid == 0) { + return; + } + if (!playlistEndReached[0]) { + loadingPlaylist = true; + AccountInstance.getInstance(playingMessageObject.currentAccount).getMediaDataController().loadMedia(playingMessageObject.getDialogId(), 50, playlistMaxId[0], MediaDataController.MEDIA_MUSIC, 1, playlistClassGuid); + } else if (playlistMergeDialogId != 0 && !playlistEndReached[1]) { + loadingPlaylist = true; + AccountInstance.getInstance(playingMessageObject.currentAccount).getMediaDataController().loadMedia(playlistMergeDialogId, 50, playlistMaxId[0], MediaDataController.MEDIA_MUSIC, 1, playlistClassGuid); + } } - public boolean setPlaylist(ArrayList messageObjects, MessageObject current, boolean loadMusic) { + public boolean setPlaylist(ArrayList messageObjects, MessageObject current, long mergeDialogId) { + return setPlaylist(messageObjects, current, mergeDialogId, true); + } + + public boolean setPlaylist(ArrayList messageObjects, MessageObject current, long mergeDialogId, boolean loadMusic) { if (playingMessageObject == current) { return playMessage(current); } forceLoopCurrentPlaylist = !loadMusic; + playlistMergeDialogId = mergeDialogId; playMusicAgain = !playlist.isEmpty(); - playlist.clear(); + clearPlaylist(); for (int a = messageObjects.size() - 1; a >= 0; a--) { MessageObject messageObject = messageObjects.get(a); if (messageObject.isMusic()) { playlist.add(messageObject); + playlistMap.put(messageObject.getId(), messageObject); } } currentPlaylistNum = playlist.indexOf(current); if (currentPlaylistNum == -1) { - playlist.clear(); - shuffledPlaylist.clear(); + clearPlaylist(); currentPlaylistNum = playlist.size(); playlist.add(current); + playlistMap.put(current.getId(), current); } if (current.isMusic() && !current.scheduled) { if (SharedConfig.shuffleMusic) { @@ -1836,7 +1942,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, currentPlaylistNum = 0; } if (loadMusic) { - MediaDataController.getInstance(current.currentAccount).loadMusic(current.getDialogId(), playlist.get(0).getIdWithChannel()); + MediaDataController.getInstance(current.currentAccount).loadMusic(current.getDialogId(), playlist.get(0).getIdWithChannel(), playlist.get(playlist.size() - 1).getIdWithChannel()); } } return playMessage(current); @@ -2225,8 +2331,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, playerWasReady = false; boolean destroyAtEnd = true; int[] playCount = null; - playlist.clear(); - shuffledPlaylist.clear(); + clearPlaylist(); videoPlayer = player; playingMessageObject = messageObject; int tag = ++playerNum; @@ -2438,8 +2543,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, playerWasReady = false; boolean destroyAtEnd = !isVideo || messageObject.messageOwner.to_id.channel_id == 0 && messageObject.audioProgress <= 0.1f; int[] playCount = isVideo && messageObject.getDuration() <= 30 ? new int[]{1} : null; - playlist.clear(); - shuffledPlaylist.clear(); + clearPlaylist(); videoPlayer = new VideoPlayer(); int tag = ++playerNum; videoPlayer.setDelegate(new VideoPlayer.VideoPlayerDelegate() { @@ -2652,6 +2756,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, AndroidUtilities.runOnUIThread(() -> NotificationCenter.getInstance(messageObject.currentAccount).postNotificationName(NotificationCenter.fileDidLoad, FileLoader.getAttachFileName(messageObject.getDocument()), cacheFile)); } audioPlayer.preparePlayer(Uri.fromFile(cacheFile), "other"); + isStreamingCurrentAudio = false; } else { int reference = FileLoader.getInstance(messageObject.currentAccount).getFileReference(messageObject); TLRPC.Document document = messageObject.getDocument(); @@ -2666,14 +2771,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, "&reference=" + Utilities.bytesToHex(document.file_reference != null ? document.file_reference : new byte[0]); Uri uri = Uri.parse("tg://" + messageObject.getFileName() + params); audioPlayer.preparePlayer(uri, "other"); + isStreamingCurrentAudio = true; } if (messageObject.isVoice()) { if (currentPlaybackSpeed > 1.0f) { audioPlayer.setPlaybackSpeed(currentPlaybackSpeed); } audioInfo = null; - playlist.clear(); - shuffledPlaylist.clear(); + clearPlaylist(); } else { try { audioInfo = AudioInfo.getAudioInfo(cacheFile); @@ -2789,9 +2894,9 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return audioInfo; } - public void toggleShuffleMusic(int type) { + public void setPlaybackOrderType(int type) { boolean oldShuffle = SharedConfig.shuffleMusic; - SharedConfig.toggleShuffleMusic(type); + SharedConfig.setPlaybackOrderType(type); if (oldShuffle != SharedConfig.shuffleMusic) { if (SharedConfig.shuffleMusic) { buildShuffledPlayList(); @@ -2800,8 +2905,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (playingMessageObject != null) { currentPlaylistNum = playlist.indexOf(playingMessageObject); if (currentPlaylistNum == -1) { - playlist.clear(); - shuffledPlaylist.clear(); + clearPlaylist(); cleanupPlayer(true, true); } } @@ -2809,6 +2913,10 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } } + public boolean isStreamingCurrentAudio() { + return isStreamingCurrentAudio; + } + public boolean isCurrentPlayer(VideoPlayer player) { return videoPlayer == player || audioPlayer == player; } @@ -3724,7 +3832,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return -5; } - private void didWriteData(final VideoConvertMessage message, final File file, final boolean last, long availableSize, final boolean error, final float progress) { + private void didWriteData(final VideoConvertMessage message, final File file, final boolean last, final long lastFrameTimestamp, long availableSize, final boolean error, final float progress) { final boolean firstWrite = message.videoEditedInfo.videoConvertFirstWrite; if (firstWrite) { message.videoEditedInfo.videoConvertFirstWrite = false; @@ -3738,12 +3846,12 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, startVideoConvertFromQueue(); } if (error) { - NotificationCenter.getInstance(message.currentAccount).postNotificationName(NotificationCenter.filePreparingFailed, message.messageObject, file.toString(), progress); + NotificationCenter.getInstance(message.currentAccount).postNotificationName(NotificationCenter.filePreparingFailed, message.messageObject, file.toString(), progress, lastFrameTimestamp); } else { if (firstWrite) { - NotificationCenter.getInstance(message.currentAccount).postNotificationName(NotificationCenter.filePreparingStarted, message.messageObject, file.toString(), progress); + NotificationCenter.getInstance(message.currentAccount).postNotificationName(NotificationCenter.filePreparingStarted, message.messageObject, file.toString(), progress, lastFrameTimestamp); } - NotificationCenter.getInstance(message.currentAccount).postNotificationName(NotificationCenter.fileNewChunkAvailable, message.messageObject, file.toString(), availableSize, last ? file.length() : 0, progress); + NotificationCenter.getInstance(message.currentAccount).postNotificationName(NotificationCenter.fileNewChunkAvailable, message.messageObject, file.toString(), availableSize, last ? file.length() : 0, progress, lastFrameTimestamp); } }); } @@ -3784,6 +3892,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } String videoPath = info.originalPath; long startTime = info.startTime; + long avatarStartTime = info.avatarStartTime; long endTime = info.endTime; int resultWidth = info.resultWidth; int resultHeight = info.resultHeight; @@ -3792,12 +3901,15 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, int originalHeight = info.originalHeight; int framerate = info.framerate; int bitrate = info.bitrate; - int rotateRender = 0; + int originalBitrate = info.originalBitrate; boolean isSecret = ((int) messageObject.getDialogId()) == 0; final File cacheFile = new File(messageObject.messageOwner.attachPath); if (cacheFile.exists()) { cacheFile.delete(); } + if (BuildVars.LOGS_ENABLED) { + FileLog.d("begin convert " + videoPath + " startTime = " + startTime + " avatarStartTime = " + avatarStartTime + " endTime " + endTime + " rWidth = " + resultWidth + " rHeight = " + resultHeight + " rotation = " + rotationValue + " oWidth = " + originalWidth + " oHeight = " + originalHeight + " framerate = " + framerate + " bitrate = " + bitrate + " originalBitrate = " + originalBitrate); + } if (videoPath == null) { videoPath = ""; @@ -3820,25 +3932,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, framerate = 59; } - if (rotationValue == 90) { + if (rotationValue == 90 || rotationValue == 270) { int temp = resultHeight; resultHeight = resultWidth; resultWidth = temp; - rotationValue = 0; - rotateRender = 270; - } else if (rotationValue == 180) { - rotateRender = 180; - rotationValue = 0; - } else if (rotationValue == 270) { - int temp = resultHeight; - resultHeight = resultWidth; - resultWidth = temp; - rotationValue = 0; - rotateRender = 90; } - boolean needCompress = info.mediaEntities != null || info.paintPath != null || info.filterState != null || resultWidth != originalWidth || resultHeight != originalHeight || rotateRender != 0 - || info.roundVideo || Build.VERSION.SDK_INT >= 18 && startTime != -1; + boolean needCompress = avatarStartTime != -1 || info.cropState != null || info.mediaEntities != null || info.paintPath != null || info.filterState != null || + resultWidth != originalWidth || resultHeight != originalHeight || rotationValue != 0 || info.roundVideo || startTime != -1; SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("videoconvert", Activity.MODE_PRIVATE); @@ -3847,7 +3948,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, VideoConvertorListener callback = new VideoConvertorListener() { - long lastAvailableSize = 0; + private long lastAvailableSize = 0; @Override public boolean checkConversionCanceled() { @@ -3868,7 +3969,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } lastAvailableSize = availableSize; - MediaController.this.didWriteData(convertMessage, cacheFile, false, availableSize, false, progress); + MediaController.this.didWriteData(convertMessage, cacheFile, false, 0, availableSize, false, progress); } }; @@ -3878,13 +3979,14 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, boolean error = videoConvertor.convertVideo(videoPath, cacheFile, rotationValue, isSecret, resultWidth, resultHeight, - framerate, bitrate, - startTime, endTime, + framerate, bitrate, originalBitrate, + startTime, endTime, avatarStartTime, needCompress, duration, info.filterState, info.paintPath, info.mediaEntities, info.isPhoto, + info.cropState, callback); @@ -3900,7 +4002,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } preferences.edit().putBoolean("isPreviousOk", true).apply(); - didWriteData(convertMessage, cacheFile, true, cacheFile.length(), error || canceled, 1f); + didWriteData(convertMessage, cacheFile, true, videoConvertor.getLastFrameTimestamp(), cacheFile.length(), error || canceled, 1f); return true; } @@ -3958,6 +4060,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, public interface VideoConvertorListener { boolean checkConversionCanceled(); - void didWriteData(long availableSize,float progress); + void didWriteData(long availableSize, float progress); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java index 83e9eac2c..e6a7058cf 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java @@ -12,7 +12,6 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -26,7 +25,6 @@ import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.os.Build; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -66,14 +64,21 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import tw.nekomimi.nekogram.NekoConfig; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.drawable.IconCompat; + @SuppressWarnings("unchecked") public class MediaDataController extends BaseController { + public static String SHORTCUT_CATEGORY = "org.telegram.messenger.SHORTCUT_SHARE"; + private static volatile MediaDataController[] Instance = new MediaDataController[UserConfig.MAX_ACCOUNT_COUNT]; public static MediaDataController getInstance(int num) { MediaDataController localInstance = Instance[num]; @@ -207,8 +212,7 @@ public class MediaDataController extends BaseController { if (Build.VERSION.SDK_INT >= 25) { Utilities.globalQueue.postRunnable(() -> { try { - ShortcutManager shortcutManager = ApplicationLoader.applicationContext.getSystemService(ShortcutManager.class); - shortcutManager.removeAllDynamicShortcuts(); + ShortcutManagerCompat.removeAllDynamicShortcuts(ApplicationLoader.applicationContext); } catch (Exception e) { FileLog.e(e); } @@ -684,11 +688,15 @@ public class MediaDataController extends BaseController { } public static int calcDocumentsHash(ArrayList arrayList) { + return calcDocumentsHash(arrayList, 200); + } + + public static int calcDocumentsHash(ArrayList arrayList, int maxCount) { if (arrayList == null) { return 0; } long acc = 0; - for (int a = 0; a < Math.min(200, arrayList.size()); a++) { + for (int a = 0, N = Math.min(maxCount, arrayList.size()); a < N; a++) { TLRPC.Document document = arrayList.get(a); if (document == null) { continue; @@ -2231,7 +2239,6 @@ public class MediaDataController extends BaseController { FileLog.d("load media did " + uid + " count = " + count + " max_id " + max_id + " type = " + type + " cache = " + fromCache + " classGuid = " + classGuid); } int lower_part = (int)uid; - fromCache = 0; if (fromCache != 0 || lower_part == 0) { loadMediaDatabase(uid, count, max_id, type, classGuid, isChannel, fromCache); } else { @@ -2593,7 +2600,6 @@ public class MediaDataController extends BaseController { cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM media_holes_v2 WHERE uid = %d AND type = %d AND start IN (0, 1)", uid, type)); if (cursor.next()) { isEnd = cursor.intValue(0) == 1; - cursor.dispose(); } else { cursor.dispose(); cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM media_v2 WHERE uid = %d AND type = %d AND mid > 0", uid, type)); @@ -2610,11 +2616,11 @@ public class MediaDataController extends BaseController { state.dispose(); } } - cursor.dispose(); } + cursor.dispose(); + long holeMessageId = 0; if (messageMaxId != 0) { - long holeMessageId = 0; cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM media_holes_v2 WHERE uid = %d AND type = %d AND end <= %d ORDER BY end DESC LIMIT 1", uid, type, max_id)); if (cursor.next()) { holeMessageId = cursor.intValue(0); @@ -2629,7 +2635,6 @@ public class MediaDataController extends BaseController { cursor = database.queryFinalized(String.format(Locale.US, "SELECT data, mid FROM media_v2 WHERE uid = %d AND mid > 0 AND mid < %d AND type = %d ORDER BY date DESC, mid DESC LIMIT %d", uid, messageMaxId, type, countToLoad)); } } else { - long holeMessageId = 0; cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(end) FROM media_holes_v2 WHERE uid = %d AND type = %d", uid, type)); if (cursor.next()) { holeMessageId = cursor.intValue(0); @@ -2746,36 +2751,48 @@ public class MediaDataController extends BaseController { }); } - public void loadMusic(final long uid, final long max_id) { + public void loadMusic(final long uid, final long maxId, final long minId) { getMessagesStorage().getStorageQueue().postRunnable(() -> { - final ArrayList arrayList = new ArrayList<>(); + final ArrayList arrayListBegin = new ArrayList<>(); + final ArrayList arrayListEnd = new ArrayList<>(); try { int lower_id = (int) uid; - SQLiteCursor cursor; - if (lower_id != 0) { - cursor = getMessagesStorage().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid FROM media_v2 WHERE uid = %d AND mid < %d AND type = %d ORDER BY date DESC, mid DESC LIMIT 1000", uid, max_id, MEDIA_MUSIC)); - } else { - cursor = getMessagesStorage().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid FROM media_v2 WHERE uid = %d AND mid > %d AND type = %d ORDER BY date DESC, mid DESC LIMIT 1000", uid, max_id, MEDIA_MUSIC)); - } - - while (cursor.next()) { - NativeByteBuffer data = cursor.byteBufferValue(0); - if (data != null) { - TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); - message.readAttachPath(data, getUserConfig().clientUserId); - data.reuse(); - if (MessageObject.isMusicMessage(message)) { - message.id = cursor.intValue(1); - message.dialog_id = uid; - arrayList.add(0, new MessageObject(currentAccount, message, false)); + for (int a = 0; a < 2; a++) { + ArrayList arrayList = a == 0 ? arrayListBegin : arrayListEnd; + SQLiteCursor cursor; + if (a == 0) { + if (lower_id != 0) { + cursor = getMessagesStorage().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid FROM media_v2 WHERE uid = %d AND mid < %d AND type = %d ORDER BY date DESC, mid DESC LIMIT 1000", uid, maxId, MEDIA_MUSIC)); + } else { + cursor = getMessagesStorage().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid FROM media_v2 WHERE uid = %d AND mid > %d AND type = %d ORDER BY date DESC, mid DESC LIMIT 1000", uid, maxId, MEDIA_MUSIC)); + } + } else { + if (lower_id != 0) { + cursor = getMessagesStorage().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid FROM media_v2 WHERE uid = %d AND mid > %d AND type = %d ORDER BY date DESC, mid DESC LIMIT 1000", uid, minId, MEDIA_MUSIC)); + } else { + cursor = getMessagesStorage().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data, mid FROM media_v2 WHERE uid = %d AND mid < %d AND type = %d ORDER BY date DESC, mid DESC LIMIT 1000", uid, minId, MEDIA_MUSIC)); } } + + while (cursor.next()) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { + TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + message.readAttachPath(data, getUserConfig().clientUserId); + data.reuse(); + if (MessageObject.isMusicMessage(message)) { + message.id = cursor.intValue(1); + message.dialog_id = uid; + arrayList.add(0, new MessageObject(currentAccount, message, false)); + } + } + } + cursor.dispose(); } - cursor.dispose(); } catch (Exception e) { FileLog.e(e); } - AndroidUtilities.runOnUIThread(() -> getNotificationCenter().postNotificationName(NotificationCenter.musicDidLoad, uid, arrayList)); + AndroidUtilities.runOnUIThread(() -> getNotificationCenter().postNotificationName(NotificationCenter.musicDidLoad, uid, arrayListBegin, arrayListEnd)); }); } //---------------- MEDIA END ---------------- @@ -2790,22 +2807,30 @@ public class MediaDataController extends BaseController { private static Path roundPath; public void buildShortcuts() { - if (Build.VERSION.SDK_INT < 25) { + if (Build.VERSION.SDK_INT < 23) { return; } + int maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(ApplicationLoader.applicationContext) - 2; + if (maxShortcuts <= 0) { + maxShortcuts = 5; + } final ArrayList hintsFinal = new ArrayList<>(); if (SharedConfig.passcodeHash.length() <= 0) { for (int a = 0; a < hints.size(); a++) { hintsFinal.add(hints.get(a)); - if (hintsFinal.size() == 3) { + if (hintsFinal.size() == maxShortcuts - 2) { break; } } } Utilities.globalQueue.postRunnable(() -> { try { - ShortcutManager shortcutManager = ApplicationLoader.applicationContext.getSystemService(ShortcutManager.class); - List currentShortcuts = shortcutManager.getDynamicShortcuts(); + if (SharedConfig.directShareHash == null) { + SharedConfig.directShareHash = UUID.randomUUID().toString(); + ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).edit().putString("directShareHash2", SharedConfig.directShareHash).commit(); + } + + List currentShortcuts = ShortcutManagerCompat.getDynamicShortcuts(ApplicationLoader.applicationContext); ArrayList shortcutsToUpdate = new ArrayList<>(); ArrayList newShortcutsIds = new ArrayList<>(); ArrayList shortcutsToDelete = new ArrayList<>(); @@ -2823,7 +2848,7 @@ public class MediaDataController extends BaseController { did = -hint.peer.channel_id; } } - newShortcutsIds.add("did" + did); + newShortcutsIds.add("did3_" + did); } for (int a = 0; a < currentShortcuts.size(); a++) { String id = currentShortcuts.get(a).getId(); @@ -2839,24 +2864,27 @@ public class MediaDataController extends BaseController { Intent intent = new Intent(ApplicationLoader.applicationContext, LaunchActivity.class); intent.setAction("new_dialog"); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(new ShortcutInfo.Builder(ApplicationLoader.applicationContext, "compose") + ArrayList arrayList = new ArrayList<>(); + arrayList.add(new ShortcutInfoCompat.Builder(ApplicationLoader.applicationContext, "compose") .setShortLabel(LocaleController.getString("NewConversationShortcut", R.string.NewConversationShortcut)) .setLongLabel(LocaleController.getString("NewConversationShortcut", R.string.NewConversationShortcut)) - .setIcon(Icon.createWithResource(ApplicationLoader.applicationContext, R.drawable.shortcut_compose)) + .setIcon(IconCompat.createWithResource(ApplicationLoader.applicationContext, R.drawable.shortcut_compose)) .setIntent(intent) .build()); if (shortcutsToUpdate.contains("compose")) { - shortcutManager.updateShortcuts(arrayList); + ShortcutManagerCompat.updateShortcuts(ApplicationLoader.applicationContext, arrayList); } else { - shortcutManager.addDynamicShortcuts(arrayList); + ShortcutManagerCompat.addDynamicShortcuts(ApplicationLoader.applicationContext, arrayList); } arrayList.clear(); if (!shortcutsToDelete.isEmpty()) { - shortcutManager.removeDynamicShortcuts(shortcutsToDelete); + ShortcutManagerCompat.removeDynamicShortcuts(ApplicationLoader.applicationContext, shortcutsToDelete); } + HashSet category = new HashSet<>(1); + category.add(SHORTCUT_CATEGORY); + for (int a = 0; a < hintsFinal.size(); a++) { Intent shortcutIntent = new Intent(ApplicationLoader.applicationContext, OpenChatReceiver.class); TLRPC.TL_topPeer hint = hintsFinal.get(a); @@ -2898,6 +2926,8 @@ public class MediaDataController extends BaseController { shortcutIntent.putExtra("currentAccount", currentAccount); shortcutIntent.setAction("com.tmessages.openchat" + did); + shortcutIntent.putExtra("dialogId", did); + shortcutIntent.putExtra("hash", SharedConfig.directShareHash); shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); Bitmap bitmap = null; @@ -2933,24 +2963,27 @@ public class MediaDataController extends BaseController { } } - String id = "did" + did; + String id = "did3_" + did; if (TextUtils.isEmpty(name)) { name = " "; } - ShortcutInfo.Builder builder = new ShortcutInfo.Builder(ApplicationLoader.applicationContext, id) + ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(ApplicationLoader.applicationContext, id) .setShortLabel(name) .setLongLabel(name) .setIntent(shortcutIntent); + if (SharedConfig.directShare) { + builder.setCategories(category); + } if (bitmap != null) { - builder.setIcon(Icon.createWithBitmap(bitmap)); + builder.setIcon(IconCompat.createWithBitmap(bitmap)); } else { - builder.setIcon(Icon.createWithResource(ApplicationLoader.applicationContext, R.drawable.shortcut_user)); + builder.setIcon(IconCompat.createWithResource(ApplicationLoader.applicationContext, R.drawable.shortcut_user)); } arrayList.add(builder.build()); if (shortcutsToUpdate.contains(id)) { - shortcutManager.updateShortcuts(arrayList); + ShortcutManagerCompat.updateShortcuts(ApplicationLoader.applicationContext, arrayList); } else { - shortcutManager.addDynamicShortcuts(arrayList); + ShortcutManagerCompat.addDynamicShortcuts(ApplicationLoader.applicationContext, arrayList); } arrayList.clear(); } @@ -3437,31 +3470,30 @@ public class MediaDataController extends BaseController { } } if (Build.VERSION.SDK_INT >= 26) { - ShortcutInfo.Builder pinShortcutInfo = - new ShortcutInfo.Builder(ApplicationLoader.applicationContext, "sdid_" + did) + ShortcutInfoCompat.Builder pinShortcutInfo = + new ShortcutInfoCompat.Builder(ApplicationLoader.applicationContext, "sdid_" + did) .setShortLabel(name) .setIntent(shortcutIntent); if (bitmap != null) { - pinShortcutInfo.setIcon(Icon.createWithBitmap(bitmap)); + pinShortcutInfo.setIcon(IconCompat.createWithBitmap(bitmap)); } else { if (user != null) { if (user.bot) { - pinShortcutInfo.setIcon(Icon.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_bot)); + pinShortcutInfo.setIcon(IconCompat.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_bot)); } else { - pinShortcutInfo.setIcon(Icon.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_user)); + pinShortcutInfo.setIcon(IconCompat.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_user)); } } else if (chat != null) { if (ChatObject.isChannel(chat) && !chat.megagroup) { - pinShortcutInfo.setIcon(Icon.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_channel)); + pinShortcutInfo.setIcon(IconCompat.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_channel)); } else { - pinShortcutInfo.setIcon(Icon.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_group)); + pinShortcutInfo.setIcon(IconCompat.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_group)); } } } - ShortcutManager shortcutManager = ApplicationLoader.applicationContext.getSystemService(ShortcutManager.class); - shortcutManager.requestPinShortcut(pinShortcutInfo.build(), null); + ShortcutManagerCompat.requestPinShortcut(ApplicationLoader.applicationContext, pinShortcutInfo.build(), null); } else { Intent addIntent = new Intent(); if (bitmap != null) { @@ -3497,10 +3529,14 @@ public class MediaDataController extends BaseController { public void uninstallShortcut(long did) { try { if (Build.VERSION.SDK_INT >= 26) { - ShortcutManager shortcutManager = ApplicationLoader.applicationContext.getSystemService(ShortcutManager.class); ArrayList arrayList = new ArrayList<>(); arrayList.add("sdid_" + did); - shortcutManager.removeDynamicShortcuts(arrayList); + arrayList.add("ndid_" + did); + ShortcutManagerCompat.removeDynamicShortcuts(ApplicationLoader.applicationContext, arrayList); + if (Build.VERSION.SDK_INT >= 30) { + ShortcutManager shortcutManager = ApplicationLoader.applicationContext.getSystemService(ShortcutManager.class); + shortcutManager.removeLongLivedShortcuts(arrayList); + } } else { int lower_id = (int) did; int high_id = (int) (did >> 32); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java index 6e9f8f5dc..689b124ed 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java @@ -159,6 +159,7 @@ public class MessageObject { private boolean layoutCreated; private int generatedWithMinSize; private float generatedWithDensity; + public boolean wasJustSent; public static Pattern urlPattern; public static Pattern instagramUrlPattern; @@ -789,6 +790,15 @@ public class MessageObject { public float captionEnterProgress = 1f; public boolean drawCaptionLayout; public boolean isNewGroup; + + public void reset() { + captionEnterProgress = 1f; + offsetBottom = 0; + offsetTop = 0; + offsetRight = 0; + offsetLeft = 0; + backgroundChangeBounds = false; + } } } @@ -885,7 +895,7 @@ public class MessageObject { messageText = Emoji.replaceEmoji(messageText, paint.getFontMetricsInt(), AndroidUtilities.dp(20), false, emojiOnly); checkEmojiOnly(emojiOnly); emojiAnimatedSticker = null; - if (emojiOnlyCount == 1 && !(message.media instanceof TLRPC.TL_messageMediaWebPage) && message.entities.isEmpty()) { + if (emojiOnlyCount == 1 && !(message.media instanceof TLRPC.TL_messageMediaWebPage) && !(message.media instanceof TLRPC.TL_messageMediaInvoice) && message.entities.isEmpty()) { CharSequence emoji = messageText; int index; if ((index = TextUtils.indexOf(emoji, "\uD83C\uDFFB")) >= 0) { @@ -1022,9 +1032,17 @@ public class MessageObject { messageOwner.action.photo = event.action.new_photo; if (chat.megagroup) { - messageText = replaceWithLink(LocaleController.getString("EventLogEditedGroupPhoto", R.string.EventLogEditedGroupPhoto), "un1", fromUser); + if (isVideoAvatar()) { + messageText = replaceWithLink(LocaleController.getString("EventLogEditedGroupVideo", R.string.EventLogEditedGroupVideo), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogEditedGroupPhoto", R.string.EventLogEditedGroupPhoto), "un1", fromUser); + } } else { - messageText = replaceWithLink(LocaleController.getString("EventLogEditedChannelPhoto", R.string.EventLogEditedChannelPhoto), "un1", fromUser); + if (isVideoAvatar()) { + messageText = replaceWithLink(LocaleController.getString("EventLogEditedChannelVideo", R.string.EventLogEditedChannelVideo), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("EventLogEditedChannelPhoto", R.string.EventLogEditedChannelPhoto), "un1", fromUser); + } } } } else if (event.action instanceof TLRPC.TL_channelAdminLogEventActionParticipantJoin) { @@ -2144,6 +2162,10 @@ public class MessageObject { } } + public boolean isVideoAvatar() { + return messageOwner.action != null && messageOwner.action.photo != null && !messageOwner.action.photo.video_sizes.isEmpty(); + } + public boolean isFcmMessage() { return localType != 0; } @@ -2262,12 +2284,24 @@ public class MessageObject { } } else if (messageOwner.action instanceof TLRPC.TL_messageActionChatEditPhoto) { if (messageOwner.to_id.channel_id != 0 && !isMegagroup()) { - messageText = LocaleController.getString("ActionChannelChangedPhoto", R.string.ActionChannelChangedPhoto); + if (isVideoAvatar()) { + messageText = LocaleController.getString("ActionChannelChangedVideo", R.string.ActionChannelChangedVideo); + } else { + messageText = LocaleController.getString("ActionChannelChangedPhoto", R.string.ActionChannelChangedPhoto); + } } else { if (isOut()) { - messageText = LocaleController.getString("ActionYouChangedPhoto", R.string.ActionYouChangedPhoto); + if (isVideoAvatar()) { + messageText = LocaleController.getString("ActionYouChangedVideo", R.string.ActionYouChangedVideo); + } else { + messageText = LocaleController.getString("ActionYouChangedPhoto", R.string.ActionYouChangedPhoto); + } } else { - messageText = replaceWithLink(LocaleController.getString("ActionChangedPhoto", R.string.ActionChangedPhoto), "un1", fromUser); + if (isVideoAvatar()) { + messageText = replaceWithLink(LocaleController.getString("ActionChangedVideo", R.string.ActionChangedVideo), "un1", fromUser); + } else { + messageText = replaceWithLink(LocaleController.getString("ActionChangedPhoto", R.string.ActionChangedPhoto), "un1", fromUser); + } } } } else if (messageOwner.action instanceof TLRPC.TL_messageActionChatEditTitle) { @@ -2863,6 +2897,9 @@ public class MessageObject { if (photo.dc_id != 0) { for (int a = 0, N = photoThumbs.size(); a < N; a++) { TLRPC.FileLocation location = photoThumbs.get(a).location; + if (location == null) { + continue; + } location.dc_id = photo.dc_id; location.file_reference = photo.file_reference; } @@ -4371,7 +4408,7 @@ public class MessageObject { } public static boolean canAutoplayAnimatedSticker(TLRPC.Document document) { - return isAnimatedStickerDocument(document, true) && SharedConfig.getDevicePerfomanceClass() != SharedConfig.PERFORMANCE_CLASS_LOW; + return isAnimatedStickerDocument(document, true) && SharedConfig.getDevicePerformanceClass() != SharedConfig.PERFORMANCE_CLASS_LOW; } public static boolean isMaskDocument(TLRPC.Document document) { @@ -4430,7 +4467,7 @@ public class MessageObject { return false; } - public static TLRPC.TL_videoSize getDocumentVideoThumb(TLRPC.Document document) { + public static TLRPC.VideoSize getDocumentVideoThumb(TLRPC.Document document) { if (document == null || document.video_thumbs.isEmpty()) { return null; } @@ -4764,7 +4801,7 @@ public class MessageObject { if (TextUtils.isEmpty(messageMediaDice.emoticon)) { return "\uD83C\uDFB2"; } - return messageMediaDice.emoticon; + return messageMediaDice.emoticon.replace("\ufe0f", ""); } public int getDiceValue() { @@ -5202,6 +5239,9 @@ public class MessageObject { } public static boolean canDeleteMessage(int currentAccount, boolean inScheduleMode, TLRPC.Message message, TLRPC.Chat chat) { + if (message == null) { + return false; + } if (message.id < 0) { return true; } @@ -5344,6 +5384,12 @@ public class MessageObject { if (currentPhotoObject != null) { mediaExists = FileLoader.getPathToAttach(currentPhotoObject, true).exists(); } + } else if (type == 11) { + TLRPC.Photo photo = messageOwner.action.photo; + if (photo == null || photo.video_sizes.isEmpty()) { + return; + } + mediaExists = FileLoader.getPathToAttach(photo.video_sizes.get(0), true).exists(); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java index f5f0247f0..78cc9357d 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java @@ -169,6 +169,7 @@ public class MessagesController extends BaseController implements NotificationCe private LongSparseArray> reloadingScheduledWebpagesPending = new LongSparseArray<>(); private LongSparseArray lastScheduledServerQueryTime = new LongSparseArray<>(); + private LongSparseArray lastServerQueryTime = new LongSparseArray<>(); private LongSparseArray> reloadingMessages = new LongSparseArray<>(); @@ -200,6 +201,9 @@ public class MessagesController extends BaseController implements NotificationCe private TLRPC.messages_Dialogs resetDialogsAll; private SparseIntArray loadingPinnedDialogs = new SparseIntArray(); + public ArrayList faqSearchArray = new ArrayList<>(); + public TLRPC.WebPage faqWebPage; + private int loadingNotificationSettings; private boolean loadingNotificationSignUpSettings; @@ -289,6 +293,8 @@ public class MessagesController extends BaseController implements NotificationCe public boolean qrLoginCamera; public boolean saveGifsWithStickers; private String installReferer; + public Set pendingSuggestions; + public boolean autoarchiveAvailable; public ArrayList gifSearchEmojies = new ArrayList<>(); public HashSet diceEmojies; public HashMap diceSuccess = new HashMap<>(); @@ -297,6 +303,45 @@ public class MessagesController extends BaseController implements NotificationCe private SharedPreferences mainPreferences; private SharedPreferences emojiPreferences; + public static class FaqSearchResult { + + public String title; + public String[] path; + public String url; + public int num; + + public FaqSearchResult(String t, String[] p, String u) { + title = t; + path = p; + url = u; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FaqSearchResult)) { + return false; + } + FaqSearchResult result = (FaqSearchResult) obj; + return title.equals(result.title); + } + + @Override + public String toString() { + SerializedData data = new SerializedData(); + data.writeInt32(num); + data.writeInt32(0); + data.writeString(title); + data.writeInt32(path != null ? path.length : 0); + if (path != null) { + for (int a = 0; a < path.length; a++) { + data.writeString(path[a]); + } + } + data.writeString(url); + return Utilities.bytesToHex(data.toByteArray()); + } + } + public static class DiceFrameSuccess { public int frame; public int num; @@ -720,6 +765,13 @@ public class MessagesController extends BaseController implements NotificationCe saveGifsWithStickers = mainPreferences.getBoolean("saveGifsWithStickers", false); filtersEnabled = mainPreferences.getBoolean("filtersEnabled", false); showFiltersTooltip = mainPreferences.getBoolean("showFiltersTooltip", false); + autoarchiveAvailable = mainPreferences.getBoolean("autoarchiveAvailable", false); + pendingSuggestions = mainPreferences.getStringSet("pendingSuggestions", null); + if (pendingSuggestions != null) { + pendingSuggestions = new HashSet<>(pendingSuggestions); + } else { + pendingSuggestions = new HashSet<>(); + } Set emojies = mainPreferences.getStringSet("diceEmojies", null); if (emojies == null) { @@ -1435,6 +1487,37 @@ public class MessagesController extends BaseController implements NotificationCe } break; } + case "autoarchive_setting_available": { + if (value.value instanceof TLRPC.TL_jsonBool) { + TLRPC.TL_jsonBool bool = (TLRPC.TL_jsonBool) value.value; + if (bool.value != autoarchiveAvailable) { + autoarchiveAvailable = bool.value; + editor.putBoolean("autoarchiveAvailable", autoarchiveAvailable); + changed = true; + } + } + break; + } + case "pending_suggestions": { + HashSet newSuggestions = new HashSet<>(); + if (value.value instanceof TLRPC.TL_jsonArray) { + TLRPC.TL_jsonArray array = (TLRPC.TL_jsonArray) value.value; + for (int b = 0, N2 = array.value.size(); b < N2; b++) { + TLRPC.JSONValue val = array.value.get(b); + if (val instanceof TLRPC.TL_jsonString) { + TLRPC.TL_jsonString string = (TLRPC.TL_jsonString) val; + newSuggestions.add(string.value); + } + } + } + if (!pendingSuggestions.equals(newSuggestions)) { + pendingSuggestions = newSuggestions; + editor.putStringSet("pendingSuggestions", pendingSuggestions); + getNotificationCenter().postNotificationName(NotificationCenter.newSuggestionsAvailable); + changed = true; + } + break; + } } } if (changed) { @@ -1450,6 +1533,22 @@ public class MessagesController extends BaseController implements NotificationCe })); } + public void removeSuggestion(String suggestion) { + if (suggestion == null) { + return; + } + if (pendingSuggestions.remove(suggestion)) { + SharedPreferences.Editor editor = mainPreferences.edit(); + editor.putStringSet("pendingSuggestions", pendingSuggestions); + editor.commit(); + TLRPC.TL_help_dismissSuggestion req = new TLRPC.TL_help_dismissSuggestion(); + req.suggestion = suggestion; + getConnectionsManager().sendRequest(req, (response, error) -> { + + }); + } + } + public void updateConfig(final TLRPC.TL_config config) { AndroidUtilities.runOnUIThread(() -> { getDownloadController().loadAutoDownloadConfig(false); @@ -1713,6 +1812,7 @@ public class MessagesController extends BaseController implements NotificationCe if (uploadingAvatar != null && uploadingAvatar.equals(location)) { TLRPC.TL_photos_uploadProfilePhoto req = new TLRPC.TL_photos_uploadProfilePhoto(); req.file = file; + req.flags |= 1; getConnectionsManager().sendRequest(req, (response, error) -> { if (error == null) { TLRPC.User user = getUser(getUserConfig().getClientUserId()); @@ -3042,7 +3142,7 @@ public class MessagesController extends BaseController implements NotificationCe SharedPreferences.Editor editor = notificationsPreferences.edit(); boolean bar_hidden = !settings.report_spam && !settings.add_contact && !settings.block_contact && !settings.share_contact && !settings.report_geo; if (BuildVars.LOGS_ENABLED) { - FileLog.d("peer settings loaded for " + dialogId + " add = " + settings.add_contact + " block = " + settings.block_contact + " spam = " + settings.report_spam + " share = " + settings.share_contact + " geo = " + settings.report_geo + " hide = " + bar_hidden); + FileLog.d("peer settings loaded for " + dialogId + " add = " + settings.add_contact + " block = " + settings.block_contact + " spam = " + settings.report_spam + " share = " + settings.share_contact + " geo = " + settings.report_geo + " hide = " + bar_hidden + " distance = " + settings.geo_distance); } editor.putInt("dialog_bar_vis3" + dialogId, bar_hidden ? 1 : 2); editor.putBoolean("dialog_bar_share" + dialogId, settings.share_contact); @@ -3051,6 +3151,14 @@ public class MessagesController extends BaseController implements NotificationCe editor.putBoolean("dialog_bar_block" + dialogId, settings.block_contact); editor.putBoolean("dialog_bar_exception" + dialogId, settings.need_contacts_exception); editor.putBoolean("dialog_bar_location" + dialogId, settings.report_geo); + editor.putBoolean("dialog_bar_archived" + dialogId, settings.autoarchived); + if (notificationsPreferences.getInt("dialog_bar_distance" + dialogId, -1) != -2) { + if ((settings.flags & 64) != 0) { + editor.putInt("dialog_bar_distance" + dialogId, settings.geo_distance); + } else { + editor.remove("dialog_bar_distance" + dialogId); + } + } editor.commit(); getNotificationCenter().postNotificationName(NotificationCenter.peerSettingsDidLoad, dialogId); } @@ -3277,7 +3385,7 @@ public class MessagesController extends BaseController implements NotificationCe int reqId = getConnectionsManager().sendRequest(req, (response, error) -> { if (error == null) { TLRPC.photos_Photos res = (TLRPC.photos_Photos) response; - processLoadedUserPhotos(res, did, count, max_id, false, classGuid); + processLoadedUserPhotos(res, null, did, count, max_id, false, classGuid); } }); getConnectionsManager().bindRequestToGuid(reqId, classGuid); @@ -3292,6 +3400,7 @@ public class MessagesController extends BaseController implements NotificationCe if (error == null) { TLRPC.messages_Messages messages = (TLRPC.messages_Messages) response; TLRPC.TL_photos_photos res = new TLRPC.TL_photos_photos(); + ArrayList arrayList = new ArrayList<>(); res.count = messages.count; res.users.addAll(messages.users); for (int a = 0; a < messages.messages.size(); a++) { @@ -3300,8 +3409,9 @@ public class MessagesController extends BaseController implements NotificationCe continue; } res.photos.add(message.action.photo); + arrayList.add(message); } - processLoadedUserPhotos(res, did, count, max_id, false, classGuid); + processLoadedUserPhotos(res, arrayList, did, count, max_id, false, classGuid); } }); getConnectionsManager().bindRequestToGuid(reqId, classGuid); @@ -3509,6 +3619,7 @@ public class MessagesController extends BaseController implements NotificationCe getNotificationCenter().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_ALL); getConnectionsManager().sendRequest(req, (response, error) -> { if (error == null) { + TLRPC.TL_photos_photo photos_photo = (TLRPC.TL_photos_photo) response; TLRPC.User user1 = getUser(getUserConfig().getClientUserId()); if (user1 == null) { user1 = getUserConfig().getCurrentUser(); @@ -3523,7 +3634,16 @@ public class MessagesController extends BaseController implements NotificationCe ArrayList users = new ArrayList<>(); users.add(user1); getMessagesStorage().putUsersAndChats(users, null, false, true); - user1.photo = (TLRPC.UserProfilePhoto) response; + if (photos_photo.photo instanceof TLRPC.TL_photo) { + user1.photo = new TLRPC.TL_userProfilePhoto(); + user1.photo.has_video = !photos_photo.photo.video_sizes.isEmpty(); + user1.photo.photo_id = photos_photo.photo.id; + user1.photo.photo_small = FileLoader.getClosestPhotoSizeWithSize(photos_photo.photo.sizes, 150).location; + user1.photo.photo_big = FileLoader.getClosestPhotoSizeWithSize(photos_photo.photo.sizes, 800).location; + user1.photo.dc_id = photos_photo.photo.dc_id; + } else { + user1.photo = new TLRPC.TL_userProfilePhotoEmpty(); + } AndroidUtilities.runOnUIThread(() -> { getNotificationCenter().postNotificationName(NotificationCenter.mainUserInfoChanged); getNotificationCenter().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_ALL); @@ -3540,17 +3660,17 @@ public class MessagesController extends BaseController implements NotificationCe } } - public void processLoadedUserPhotos(final TLRPC.photos_Photos res, final int did, final int count, final long max_id, final boolean fromCache, final int classGuid) { + public void processLoadedUserPhotos(final TLRPC.photos_Photos res, ArrayList messages, final int did, final int count, final long max_id, final boolean fromCache, final int classGuid) { if (!fromCache) { getMessagesStorage().putUsersAndChats(res.users, null, true, true); - getMessagesStorage().putDialogPhotos(did, res); + getMessagesStorage().putDialogPhotos(did, res, messages); } else if (res == null || res.photos.isEmpty()) { loadDialogPhotos(did, count, max_id, false, classGuid); return; } AndroidUtilities.runOnUIThread(() -> { putUsers(res.users, fromCache); - getNotificationCenter().postNotificationName(NotificationCenter.dialogPhotosLoaded, did, count, fromCache, classGuid, res.photos); + getNotificationCenter().postNotificationName(NotificationCenter.dialogPhotosLoaded, did, count, fromCache, classGuid, res.photos, messages); }); } @@ -5284,9 +5404,7 @@ public class MessagesController extends BaseController implements NotificationCe loadMessagesInternal(dialogId, mergeDialogId, loadInfo, count, max_id, offset_date, false, minDate, classGuid, load_type, dialog.top_message, isChannel, false, loadIndex, first_unread, dialog.unread_count, last_date, queryFromServer, dialog.unread_mentions_count, false); } } else { - AndroidUtilities.runOnUIThread(() -> { - getNotificationCenter().postNotificationName(NotificationCenter.loadingMessagesFailed, classGuid, req, error); - }); + AndroidUtilities.runOnUIThread(() -> getNotificationCenter().postNotificationName(NotificationCenter.loadingMessagesFailed, classGuid, req, error)); } }); return; @@ -5333,9 +5451,7 @@ public class MessagesController extends BaseController implements NotificationCe } processLoadedMessages(res, dialogId, mergeDialogId, count, mid, offset_date, false, classGuid, first_unread, last_message_id, unread_count, last_date, load_type, isChannel, false, false, loadIndex, queryFromServer, mentionsCount); } else { - AndroidUtilities.runOnUIThread(() -> { - getNotificationCenter().postNotificationName(NotificationCenter.loadingMessagesFailed, classGuid, req, error); - }); + AndroidUtilities.runOnUIThread(() -> getNotificationCenter().postNotificationName(NotificationCenter.loadingMessagesFailed, classGuid, req, error)); } }); getConnectionsManager().bindRequestToGuid(reqId, classGuid); @@ -5428,7 +5544,9 @@ public class MessagesController extends BaseController implements NotificationCe if (!isCache) { ImageLoader.saveMessagesThumbs(messagesRes.messages); } - if (high_id != 1 && lower_id != 0 && isCache && (messagesRes.messages.size() == 0 || scheduled && (SystemClock.elapsedRealtime() - lastScheduledServerQueryTime.get(dialogId, 0L)) > 60 * 1000)) { + boolean isInitialLoading = offset_date == 0 && max_id == 0; + + if (high_id != 1 && lower_id != 0 && isCache && ((messagesRes.messages.size() == 0 && (!isInitialLoading || (SystemClock.elapsedRealtime() - lastServerQueryTime.get(dialogId, 0L)) > 60 * 1000)) || scheduled && (SystemClock.elapsedRealtime() - lastScheduledServerQueryTime.get(dialogId, 0L)) > 60 * 1000)) { int hash; if (scheduled) { lastScheduledServerQueryTime.put(dialogId, SystemClock.elapsedRealtime()); @@ -5444,6 +5562,7 @@ public class MessagesController extends BaseController implements NotificationCe } hash = (int) h - 1; } else { + lastServerQueryTime.put(dialogId, SystemClock.elapsedRealtime()); hash = 0; } AndroidUtilities.runOnUIThread(() -> loadMessages(dialogId, mergeDialogId, false, count, load_type == 2 && queryFromServer ? first_unread : max_id, offset_date, false, hash, classGuid, load_type, last_message_id, isChannel, scheduled, loadIndex, first_unread, unread_count, last_date, queryFromServer, mentionsCount)); @@ -5776,7 +5895,7 @@ public class MessagesController extends BaseController implements NotificationCe FileLog.d("folderId = " + folderId + " load cacheOffset = " + offset + " count = " + count + " cache = " + fromCache); } if (fromCache) { - getMessagesStorage().getDialogs(folderId, offset == 0 ? 0 : nextDialogsCacheOffset.get(folderId, 0), count, offset == 0); + getMessagesStorage().getDialogs(folderId, offset == 0 ? 0 : nextDialogsCacheOffset.get(folderId, 0), count, folderId == 0 && offset == 0); } else { TLRPC.TL_messages_getDialogs req = new TLRPC.TL_messages_getDialogs(); req.limit = count; @@ -6892,7 +7011,7 @@ public class MessagesController extends BaseController implements NotificationCe getNotificationCenter().postNotificationName(NotificationCenter.needReloadRecentDialogsSearch); } else { generateUpdateMessage(); - if (!added && loadType == DIALOGS_LOAD_TYPE_CACHE) { + if (!added && loadType == DIALOGS_LOAD_TYPE_CACHE && dialogsEndReached.get(folderId)) { loadDialogs(folderId, 0, count, false); } } @@ -8258,27 +8377,36 @@ public class MessagesController extends BaseController implements NotificationCe } } - public void changeChatAvatar(int chat_id, TLRPC.InputFile uploadedAvatar, TLRPC.FileLocation smallSize, TLRPC.FileLocation bigSize) { + public void changeChatAvatar(int chat_id, TLRPC.TL_inputChatPhoto oldPhoto, TLRPC.InputFile inputPhoto, final TLRPC.InputFile inputVideo, double videoStartTimestamp, String videoPath, TLRPC.FileLocation smallSize, TLRPC.FileLocation bigSize) { TLObject request; + TLRPC.InputChatPhoto inputChatPhoto; + if (oldPhoto != null) { + inputChatPhoto = oldPhoto; + } else if (inputPhoto != null || inputVideo != null) { + TLRPC.TL_inputChatUploadedPhoto uploadedPhoto = new TLRPC.TL_inputChatUploadedPhoto(); + if (inputPhoto != null) { + uploadedPhoto.file = inputPhoto; + uploadedPhoto.flags |= 1; + } + if (inputVideo != null) { + uploadedPhoto.video = inputVideo; + uploadedPhoto.flags |= 2; + uploadedPhoto.video_start_ts = videoStartTimestamp; + uploadedPhoto.flags |= 4; + } + inputChatPhoto = uploadedPhoto; + } else { + inputChatPhoto = new TLRPC.TL_inputChatPhotoEmpty(); + } if (ChatObject.isChannel(chat_id, currentAccount)) { TLRPC.TL_channels_editPhoto req = new TLRPC.TL_channels_editPhoto(); req.channel = getInputChannel(chat_id); - if (uploadedAvatar != null) { - req.photo = new TLRPC.TL_inputChatUploadedPhoto(); - req.photo.file = uploadedAvatar; - } else { - req.photo = new TLRPC.TL_inputChatPhotoEmpty(); - } + req.photo = inputChatPhoto; request = req; } else { TLRPC.TL_messages_editChatPhoto req = new TLRPC.TL_messages_editChatPhoto(); req.chat_id = chat_id; - if (uploadedAvatar != null) { - req.photo = new TLRPC.TL_inputChatUploadedPhoto(); - req.photo.file = uploadedAvatar; - } else { - req.photo = new TLRPC.TL_inputChatPhotoEmpty(); - } + req.photo = inputChatPhoto; request = req; } getConnectionsManager().sendRequest(request, (response, error) -> { @@ -8286,41 +8414,50 @@ public class MessagesController extends BaseController implements NotificationCe return; } TLRPC.Updates updates = (TLRPC.Updates) response; - TLRPC.Photo photo = null; - for (int a = 0, N = updates.updates.size(); a < N; a++) { - TLRPC.Update update = updates.updates.get(a); - if (update instanceof TLRPC.TL_updateNewChannelMessage) { - TLRPC.Message message = ((TLRPC.TL_updateNewChannelMessage) update).message; - if (message.action instanceof TLRPC.TL_messageActionChatEditPhoto && message.action.photo instanceof TLRPC.TL_photo) { - photo = message.action.photo; - break; - } - } else if (update instanceof TLRPC.TL_updateNewMessage) { - TLRPC.Message message = ((TLRPC.TL_updateNewMessage) update).message; - if (message.action instanceof TLRPC.TL_messageActionChatEditPhoto && message.action.photo instanceof TLRPC.TL_photo) { - photo = message.action.photo; - break; + if (oldPhoto == null) { + TLRPC.Photo photo = null; + for (int a = 0, N = updates.updates.size(); a < N; a++) { + TLRPC.Update update = updates.updates.get(a); + if (update instanceof TLRPC.TL_updateNewChannelMessage) { + TLRPC.Message message = ((TLRPC.TL_updateNewChannelMessage) update).message; + if (message.action instanceof TLRPC.TL_messageActionChatEditPhoto && message.action.photo instanceof TLRPC.TL_photo) { + photo = message.action.photo; + break; + } + } else if (update instanceof TLRPC.TL_updateNewMessage) { + TLRPC.Message message = ((TLRPC.TL_updateNewMessage) update).message; + if (message.action instanceof TLRPC.TL_messageActionChatEditPhoto && message.action.photo instanceof TLRPC.TL_photo) { + photo = message.action.photo; + break; + } } } - } - if (photo != null) { - TLRPC.PhotoSize small = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 150); - if (small != null && smallSize != null) { - File destFile = FileLoader.getPathToAttach(small, true); - File src = FileLoader.getPathToAttach(smallSize, true); - src.renameTo(destFile); - String oldKey = smallSize.volume_id + "_" + smallSize.local_id + "@50_50"; - String newKey = small.location.volume_id + "_" + small.location.local_id + "@50_50"; - ImageLoader.getInstance().replaceImageInCache(oldKey, newKey, ImageLocation.getForPhoto(small, photo), true); - } - TLRPC.PhotoSize big = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 800); - if (big != null && bigSize != null) { - File destFile = FileLoader.getPathToAttach(big, true); - File src = FileLoader.getPathToAttach(bigSize, true); - src.renameTo(destFile); + if (photo != null) { + TLRPC.PhotoSize small = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 150); + TLRPC.VideoSize videoSize = photo.video_sizes.isEmpty() ? null : photo.video_sizes.get(0); + if (small != null && smallSize != null) { + File destFile = FileLoader.getPathToAttach(small, true); + File src = FileLoader.getPathToAttach(smallSize, true); + src.renameTo(destFile); + String oldKey = smallSize.volume_id + "_" + smallSize.local_id + "@50_50"; + String newKey = small.location.volume_id + "_" + small.location.local_id + "@50_50"; + ImageLoader.getInstance().replaceImageInCache(oldKey, newKey, ImageLocation.getForPhoto(small, photo), true); + } + TLRPC.PhotoSize big = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 800); + if (big != null && bigSize != null) { + File destFile = FileLoader.getPathToAttach(big, true); + File src = FileLoader.getPathToAttach(bigSize, true); + src.renameTo(destFile); + } + if (videoSize != null && videoPath != null) { + File destFile = FileLoader.getPathToAttach(videoSize, "mp4", true); + File src = new File(videoPath); + src.renameTo(destFile); + } } } processUpdates(updates, false); + AndroidUtilities.runOnUIThread(() -> getNotificationCenter().postNotificationName(NotificationCenter.updateInterfaces, MessagesController.UPDATE_MASK_AVATAR)); }, ConnectionsManager.RequestFlagInvokeAfter); } @@ -8546,7 +8683,6 @@ public class MessagesController extends BaseController implements NotificationCe if (anyProceed) { updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); } - return; } else { if (BuildVars.LOGS_ENABLED) { FileLog.d("HOLE IN CHANNEL " + channelId + " UPDATES QUEUE - getChannelDifference "); @@ -8554,8 +8690,8 @@ public class MessagesController extends BaseController implements NotificationCe updatesStartWaitTimeChannels.delete(channelId); updatesQueueChannels.remove(channelId); getChannelDifference(channelId); - return; } + return; } else { updatesQueue.remove(a); a--; @@ -8608,7 +8744,6 @@ public class MessagesController extends BaseController implements NotificationCe if (anyProceed) { setUpdatesStartTime(type, System.currentTimeMillis()); } - return; } else { if (BuildVars.LOGS_ENABLED) { FileLog.d("HOLE IN UPDATES QUEUE - getDifference"); @@ -8616,8 +8751,8 @@ public class MessagesController extends BaseController implements NotificationCe setUpdatesStartTime(type, 0); updatesQueue.clear(); getDifference(); - return; } + return; } else { updatesQueue.remove(a); a--; @@ -10384,6 +10519,33 @@ public class MessagesController extends BaseController implements NotificationCe getMessagesStorage().saveDiffParams(getMessagesStorage().getLastSeqValue(), getMessagesStorage().getLastPtsValue(), getMessagesStorage().getLastDateValue(), getMessagesStorage().getLastQtsValue()); } + private boolean applyFoldersUpdates(ArrayList folderUpdates) { + if (folderUpdates == null) { + return false; + } + boolean updated = false; + for (int a = 0, size = folderUpdates.size(); a < size; a++) { + TLRPC.TL_updateFolderPeers update = folderUpdates.get(a); + for (int b = 0, size2 = update.folder_peers.size(); b < size2; b++) { + TLRPC.TL_folderPeer folderPeer = update.folder_peers.get(b); + long dialogId = DialogObject.getPeerDialogId(folderPeer.peer); + TLRPC.Dialog dialog = dialogs_dict.get(dialogId); + if (dialog == null) { + continue; + } + if (dialog.folder_id != folderPeer.folder_id) { + dialog.pinned = false; + dialog.pinnedNum = 0; + dialog.folder_id = folderPeer.folder_id; + ensureFolderDialogExists(folderPeer.folder_id, null); + } + } + updated = true; + getMessagesStorage().setDialogsFolderId(folderUpdates.get(a).folder_peers, null, 0, 0); + } + return updated; + } + public boolean processUpdateArray(ArrayList updates, final ArrayList usersArr, final ArrayList chatsArr, boolean fromGetDifference, int date) { if (updates.isEmpty()) { if (usersArr != null || chatsArr != null) { @@ -10414,6 +10576,7 @@ public class MessagesController extends BaseController implements NotificationCe SparseIntArray clearHistoryMessages = null; ArrayList chatInfoToUpdate = null; ArrayList updatesOnMainThread = null; + ArrayList folderUpdates = null; ArrayList tasks = null; ArrayList contactsIds = null; ArrayList messageThumbs = null; @@ -11014,13 +11177,11 @@ public class MessagesController extends BaseController implements NotificationCe } updatesOnMainThread.add(baseUpdate); } else if (baseUpdate instanceof TLRPC.TL_updateFolderPeers) { - if (updatesOnMainThread == null) { - updatesOnMainThread = new ArrayList<>(); - } - updatesOnMainThread.add(baseUpdate); - TLRPC.TL_updateFolderPeers update = (TLRPC.TL_updateFolderPeers) baseUpdate; - getMessagesStorage().setDialogsFolderId(update.folder_peers, null, 0, 0); + if (folderUpdates == null) { + folderUpdates = new ArrayList<>(); + } + folderUpdates.add(update); } else if (baseUpdate instanceof TLRPC.TL_updatePrivacy) { if (updatesOnMainThread == null) { updatesOnMainThread = new ArrayList<>(); @@ -11425,6 +11586,11 @@ public class MessagesController extends BaseController implements NotificationCe if (channelViews != null) { getMessagesStorage().putChannelViews(channelViews, true); } + if (folderUpdates != null) { + for (int a = 0, size = folderUpdates.size(); a < size; a++) { + getMessagesStorage().setDialogsFolderId(folderUpdates.get(a).folder_peers, null, 0, 0); + } + } final LongSparseArray> editingMessagesFinal = editingMessages; final SparseArray channelViewsFinal = channelViews; @@ -11435,6 +11601,7 @@ public class MessagesController extends BaseController implements NotificationCe final ArrayList contactsIdsFinal = contactsIds; final ArrayList updatesOnMainThreadFinal = updatesOnMainThread; final ArrayList updateMessageThumbs = messageThumbs; + final ArrayList folderUpdatesFinal = folderUpdates; AndroidUtilities.runOnUIThread(() -> { int updateMask = interfaceUpdateMaskFinal; @@ -11560,23 +11727,6 @@ public class MessagesController extends BaseController implements NotificationCe order = null; } loadPinnedDialogs(update.folder_id, 0, order); - } else if (baseUpdate instanceof TLRPC.TL_updateFolderPeers) { - TLRPC.TL_updateFolderPeers update = (TLRPC.TL_updateFolderPeers) baseUpdate; - for (int b = 0, size2 = update.folder_peers.size(); b < size2; b++) { - TLRPC.TL_folderPeer folderPeer = update.folder_peers.get(b); - long dialogId = DialogObject.getPeerDialogId(folderPeer.peer); - TLRPC.Dialog dialog = dialogs_dict.get(dialogId); - if (dialog == null) { - continue; - } - if (dialog.folder_id != folderPeer.folder_id) { - dialog.pinned = false; - dialog.pinnedNum = 0; - dialog.folder_id = folderPeer.folder_id; - ensureFolderDialogExists(folderPeer.folder_id, null); - } - } - forceDialogsUpdate = true; } else if (baseUpdate instanceof TLRPC.TL_updateUserPhoto) { TLRPC.TL_updateUserPhoto update = (TLRPC.TL_updateUserPhoto) baseUpdate; final TLRPC.User currentUser = getUser(update.user_id); @@ -11587,6 +11737,9 @@ public class MessagesController extends BaseController implements NotificationCe toDbUser.id = update.user_id; toDbUser.photo = update.photo; dbUsers.add(toDbUser); + if (UserObject.isUserSelf(currentUser)) { + getNotificationCenter().postNotificationName(NotificationCenter.mainUserInfoChanged); + } } else if (baseUpdate instanceof TLRPC.TL_updateUserPhone) { TLRPC.TL_updateUserPhone update = (TLRPC.TL_updateUserPhone) baseUpdate; final TLRPC.User currentUser = getUser(update.user_id); @@ -11949,6 +12102,7 @@ public class MessagesController extends BaseController implements NotificationCe } } } + boolean updateDialogs = false; if (messagesFinal != null) { boolean sorted = false; @@ -11959,13 +12113,17 @@ public class MessagesController extends BaseController implements NotificationCe sorted = true; } } - if (!sorted && forceDialogsUpdate) { + boolean applied = applyFoldersUpdates(folderUpdatesFinal); + if (applied || !sorted && forceDialogsUpdate) { sortDialogs(null); } updateDialogs = true; - } else if (forceDialogsUpdate) { - sortDialogs(null); - updateDialogs = true; + } else { + boolean applied = applyFoldersUpdates(folderUpdatesFinal); + if (forceDialogsUpdate || applied) { + sortDialogs(null); + updateDialogs = true; + } } if (scheduledMessagesFinal != null) { for (int a = 0, size = scheduledMessagesFinal.size(); a < size; a++) { @@ -12490,14 +12648,12 @@ public class MessagesController extends BaseController implements NotificationCe int selfId = getUserConfig().getClientUserId(); if (selectedDialogFilter[0] != null || selectedDialogFilter[1] != null) { for (int b = 0; b < selectedDialogFilter.length; b++) { - if (selectedDialogFilter[b] == null) { + sortingDialogFilter = selectedDialogFilter[b]; + if (sortingDialogFilter == null) { continue; } - sortingDialogFilter = selectedDialogFilter[b]; - Collections.sort(allDialogs, dialogDateComparator); - - ArrayList dialogsByFilter = selectedDialogFilter[b].dialogs; + ArrayList dialogsByFilter = sortingDialogFilter.dialogs; for (int a = 0, N = allDialogs.size(); a < N; a++) { TLRPC.Dialog d = allDialogs.get(a); @@ -12510,7 +12666,7 @@ public class MessagesController extends BaseController implements NotificationCe lower_id = encryptedChat.user_id; } } - if (selectedDialogFilter[b].includesDialog(getAccountInstance(), lower_id, d)) { + if (sortingDialogFilter.includesDialog(getAccountInstance(), lower_id, d)) { dialogsByFilter.add(d); } } @@ -12859,39 +13015,42 @@ public class MessagesController extends BaseController implements NotificationCe messageId = sharedPreferences.getInt("diditem" + dialog_id, 0); } if (messageId != 0 && getMessagesStorage().checkMessageId(dialog_id, isChannel, messageId)) { - callback.run(); + if (callback != null) { + callback.run(); + } return; } int finalMessageId = messageId; final int classGuid = ConnectionsManager.generateClassGuid(); - NotificationCenter.NotificationCenterDelegate delegate = new NotificationCenter.NotificationCenterDelegate() { - @SuppressWarnings("unchecked") - @Override - public void didReceivedNotification(int id, int account, Object... args) { - if (id == NotificationCenter.messagesDidLoad && (Integer) args[10] == classGuid) { - ArrayList messArr = (ArrayList) args[2]; - boolean isCache = (Boolean) args[3]; - if (messArr.isEmpty() && isCache) { - loadMessages(dialog_id, 0, false, 20, 3, 0, false, 0, classGuid, 3, 0, false, false, 0); - } else { - getNotificationCenter().removeObserver(this, NotificationCenter.didReceiveNewMessages); + if (callback != null) { + NotificationCenter.NotificationCenterDelegate delegate = new NotificationCenter.NotificationCenterDelegate() { + @SuppressWarnings("unchecked") + @Override + public void didReceivedNotification(int id, int account, Object... args) { + if (id == NotificationCenter.messagesDidLoad && (Integer) args[10] == classGuid) { + ArrayList messArr = (ArrayList) args[2]; + boolean isCache = (Boolean) args[3]; + if (messArr.isEmpty() && isCache) { + loadMessages(dialog_id, 0, false, 20, 3, 0, false, 0, classGuid, 3, 0, false, false, 0); + } else { + getNotificationCenter().removeObserver(this, NotificationCenter.messagesDidLoad); + getNotificationCenter().removeObserver(this, NotificationCenter.loadingMessagesFailed); + callback.run(); + } + } + if (id == NotificationCenter.loadingMessagesFailed && (Integer) args[0] == classGuid) { + getNotificationCenter().removeObserver(this, NotificationCenter.messagesDidLoad); getNotificationCenter().removeObserver(this, NotificationCenter.loadingMessagesFailed); - callback.run(); + if (doOnError != null) { + doOnError.run(); + } } } - if (id == NotificationCenter.loadingMessagesFailed && (Integer) args[0] == classGuid) { - getNotificationCenter().removeObserver(this, NotificationCenter.didReceiveNewMessages); - getNotificationCenter().removeObserver(this, NotificationCenter.loadingMessagesFailed); - if (doOnError != null) { - doOnError.run(); - } - } - } - }; - - getNotificationCenter().addObserver(delegate, NotificationCenter.messagesDidLoad); - getNotificationCenter().addObserver(delegate, NotificationCenter.loadingMessagesFailed); + }; + getNotificationCenter().addObserver(delegate, NotificationCenter.messagesDidLoad); + getNotificationCenter().addObserver(delegate, NotificationCenter.loadingMessagesFailed); + } loadMessages(dialog_id, 0, false, 1, finalMessageId, 0, true, 0, classGuid, 3, 0, false, false, 0); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java index 7d2edd468..eb23ead30 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java @@ -11,6 +11,7 @@ package org.telegram.messenger; import android.content.SharedPreferences; import android.text.TextUtils; import android.util.LongSparseArray; +import android.util.Pair; import android.util.SparseArray; import android.util.SparseIntArray; @@ -74,6 +75,7 @@ public class MessagesStorage extends BaseController { private ArrayList dialogFilters = new ArrayList<>(); private SparseArray dialogFiltersMap = new SparseArray<>(); + private LongSparseArray unknownDialogsIds = new LongSparseArray<>(); private int mainUnreadCount; private int archiveUnreadCount; private volatile int pendingMainUnreadCount; @@ -263,7 +265,7 @@ public class MessagesStorage extends BaseController { try { database = new SQLiteDatabase(cacheFile.getPath()); database.executeFast("PRAGMA secure_delete = ON").stepThis().dispose(); - database.executeFast("PRAGMA temp_store = 1").stepThis().dispose(); + database.executeFast("PRAGMA temp_store = MEMORY").stepThis().dispose(); database.executeFast("PRAGMA journal_mode = WAL").stepThis().dispose(); if (createTable) { @@ -918,6 +920,7 @@ public class MessagesStorage extends BaseController { pendingArchiveUnreadCount = 0; dialogFilters.clear(); dialogFiltersMap.clear(); + unknownDialogsIds.clear(); lastSavedSeq = 0; lastSavedPts = 0; @@ -947,9 +950,6 @@ public class MessagesStorage extends BaseController { } public void cleanup(final boolean isLogin) { - if (!isLogin) { - storageQueue.cleanupQueue(); - } storageQueue.postRunnable(() -> { cleanupInternal(true); openDatabase(1); @@ -2901,6 +2901,7 @@ public class MessagesStorage extends BaseController { final ArrayList mids = new ArrayList<>(); SQLiteCursor cursor = database.queryFinalized("SELECT data FROM messages WHERE uid = " + did); ArrayList filesToDelete = new ArrayList<>(); + ArrayList> idsToDelete = new ArrayList<>(); try { while (cursor.next()) { NativeByteBuffer data = cursor.byteBufferValue(0); @@ -2910,7 +2911,7 @@ public class MessagesStorage extends BaseController { data.reuse(); if (message != null && message.from_id == uid && message.id != 1) { mids.add(message.id); - addFilesToDelete(message, filesToDelete, false); + addFilesToDelete(message, filesToDelete, idsToDelete, false); } } } @@ -2918,6 +2919,7 @@ public class MessagesStorage extends BaseController { FileLog.e(e); } cursor.dispose(); + deleteFromDownloadQueue(idsToDelete, true); AndroidUtilities.runOnUIThread(() -> getMessagesController().markChannelDialogMessageAsDeleted(mids, channelId)); markMessagesAsDeletedInternal(mids, channelId, false, false); updateDialogsWithDeletedMessagesInternal(mids, null, channelId); @@ -2931,26 +2933,52 @@ public class MessagesStorage extends BaseController { }); } - private boolean addFilesToDelete(TLRPC.Message message, ArrayList filesToDelete, boolean forceCache) { + private boolean addFilesToDelete(TLRPC.Message message, ArrayList filesToDelete, ArrayList> ids, boolean forceCache) { if (message == null) { return false; } - if (message.media instanceof TLRPC.TL_messageMediaPhoto && message.media.photo != null) { - for (int a = 0, N = message.media.photo.sizes.size(); a < N; a++) { - TLRPC.PhotoSize photoSize = message.media.photo.sizes.get(a); + int type = 0; + long id = 0; + TLRPC.Document document = MessageObject.getDocument(message); + TLRPC.Photo photo = MessageObject.getPhoto(message); + if (MessageObject.isVoiceMessage(message)) { + id = document.id; + type = DownloadController.AUTODOWNLOAD_TYPE_AUDIO; + } else if (MessageObject.isStickerMessage(message) || MessageObject.isAnimatedStickerMessage(message)) { + id = document.id; + type = DownloadController.AUTODOWNLOAD_TYPE_PHOTO; + } else if (MessageObject.isVideoMessage(message) || MessageObject.isRoundVideoMessage(message) || MessageObject.isGifMessage(message)) { + id = document.id; + type = DownloadController.AUTODOWNLOAD_TYPE_VIDEO; + } else if (document != null) { + id = document.id; + type = DownloadController.AUTODOWNLOAD_TYPE_DOCUMENT; + } else if (photo != null) { + TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, AndroidUtilities.getPhotoSize()); + if (photoSize != null) { + id = photo.id; + type = DownloadController.AUTODOWNLOAD_TYPE_PHOTO; + } + } + if (id != 0) { + ids.add(new Pair<>(id, type)); + } + if (photo != null) { + for (int a = 0, N = photo.sizes.size(); a < N; a++) { + TLRPC.PhotoSize photoSize = photo.sizes.get(a); File file = FileLoader.getPathToAttach(photoSize); if (file != null && file.toString().length() > 0) { filesToDelete.add(file); } } return true; - } else if (message.media instanceof TLRPC.TL_messageMediaDocument && message.media.document != null) { - File file = FileLoader.getPathToAttach(message.media.document, forceCache); + } else if (document != null) { + File file = FileLoader.getPathToAttach(document, forceCache); if (file != null && file.toString().length() > 0) { filesToDelete.add(file); } - for (int a = 0, N = message.media.document.thumbs.size(); a < N; a++) { - TLRPC.PhotoSize photoSize = message.media.document.thumbs.get(a); + for (int a = 0, N = document.thumbs.size(); a < N; a++) { + TLRPC.PhotoSize photoSize = document.thumbs.get(a); file = FileLoader.getPathToAttach(photoSize); if (file != null && file.toString().length() > 0) { filesToDelete.add(file); @@ -2978,6 +3006,7 @@ public class MessagesStorage extends BaseController { if ((int) did == 0 || messagesOnly == 2) { SQLiteCursor cursor = database.queryFinalized("SELECT data FROM messages WHERE uid = " + did); ArrayList filesToDelete = new ArrayList<>(); + ArrayList> idsToDelete = new ArrayList<>(); try { while (cursor.next()) { NativeByteBuffer data = cursor.byteBufferValue(0); @@ -2985,13 +3014,14 @@ public class MessagesStorage extends BaseController { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); message.readAttachPath(data, getUserConfig().clientUserId); data.reuse(); - addFilesToDelete(message, filesToDelete, false); + addFilesToDelete(message, filesToDelete, idsToDelete, false); } } } catch (Exception e) { FileLog.e(e); } cursor.dispose(); + deleteFromDownloadQueue(idsToDelete, true); getFileLoader().deleteFiles(filesToDelete, messagesOnly); } @@ -3093,18 +3123,24 @@ public class MessagesStorage extends BaseController { } final TLRPC.photos_Photos res = new TLRPC.TL_photos_photos(); + ArrayList messages = new ArrayList<>(); while (cursor.next()) { NativeByteBuffer data = cursor.byteBufferValue(0); if (data != null) { TLRPC.Photo photo = TLRPC.Photo.TLdeserialize(data, data.readInt32(false), false); + if (data.remaining() > 0) { + messages.add(TLRPC.Message.TLdeserialize(data, data.readInt32(false), false)); + } else { + messages.add(null); + } data.reuse(); res.photos.add(photo); } } cursor.dispose(); - Utilities.stageQueue.postRunnable(() -> getMessagesController().processLoadedUserPhotos(res, did, count, max_id, true, classGuid)); + Utilities.stageQueue.postRunnable(() -> getMessagesController().processLoadedUserPhotos(res, messages, did, count, max_id, true, classGuid)); } catch (Exception e) { FileLog.e(e); } @@ -3282,7 +3318,7 @@ public class MessagesStorage extends BaseController { }); } - public void putDialogPhotos(final int did, final TLRPC.photos_Photos photos) { + public void putDialogPhotos(int did, TLRPC.photos_Photos photos, ArrayList messages) { if (photos == null) { return; } @@ -3296,8 +3332,15 @@ public class MessagesStorage extends BaseController { continue; } state.requery(); - NativeByteBuffer data = new NativeByteBuffer(photo.getObjectSize()); + int size = photo.getObjectSize(); + if (messages != null) { + size += messages.get(a).getObjectSize(); + } + NativeByteBuffer data = new NativeByteBuffer(size); photo.serializeToStream(data); + if (messages != null) { + messages.get(a).serializeToStream(data); + } state.bindInteger(1, did); state.bindLong(2, photo.id); state.bindByteBuffer(3, data); @@ -3315,6 +3358,7 @@ public class MessagesStorage extends BaseController { storageQueue.postRunnable(() -> { try { ArrayList filesToDelete = new ArrayList<>(); + ArrayList> idsToDelete = new ArrayList<>(); final ArrayList messages = new ArrayList<>(); SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT data, mid, date, uid FROM messages WHERE mid IN (%s)", TextUtils.join(",", mids))); while (cursor.next()) { @@ -3324,7 +3368,7 @@ public class MessagesStorage extends BaseController { message.readAttachPath(data, getUserConfig().clientUserId); data.reuse(); if (message.media != null) { - if (!addFilesToDelete(message, filesToDelete, true)) { + if (!addFilesToDelete(message, filesToDelete, idsToDelete, true)) { continue; } else { if (message.media.document != null) { @@ -3342,6 +3386,7 @@ public class MessagesStorage extends BaseController { } } cursor.dispose(); + deleteFromDownloadQueue(idsToDelete, true); if (!messages.isEmpty()) { SQLitePreparedStatement state = database.executeFast("REPLACE INTO messages VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)"); for (int a = 0; a < messages.size(); a++) { @@ -4714,6 +4759,7 @@ public class MessagesStorage extends BaseController { state.bindLong(2, info.user.id); state.step(); state.dispose(); + unknownDialogsIds.remove(info.user.id); } } catch (Exception e) { FileLog.e(e); @@ -4773,6 +4819,7 @@ public class MessagesStorage extends BaseController { state.bindLong(2, -info.id); state.step(); state.dispose(); + unknownDialogsIds.remove(-info.id); } } catch (Exception e) { FileLog.e(e); @@ -4919,15 +4966,12 @@ public class MessagesStorage extends BaseController { TLRPC.ChatParticipant newParticipant; if (invited_id == 1) { newParticipant = new TLRPC.TL_chatParticipantAdmin(); - newParticipant.user_id = participant.user_id; - newParticipant.date = participant.date; - newParticipant.inviter_id = participant.inviter_id; } else { newParticipant = new TLRPC.TL_chatParticipant(); - newParticipant.user_id = participant.user_id; - newParticipant.date = participant.date; - newParticipant.inviter_id = participant.inviter_id; } + newParticipant.user_id = participant.user_id; + newParticipant.date = participant.date; + newParticipant.inviter_id = participant.inviter_id; info.participants.participants.set(a, newParticipant); break; } @@ -5923,7 +5967,6 @@ public class MessagesStorage extends BaseController { cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM messages_holes WHERE uid = %d AND start IN (0, 1)", dialogId)); if (cursor.next()) { isEnd = cursor.intValue(0) == 1; - cursor.dispose(); } else { cursor.dispose(); cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM messages WHERE uid = %d AND mid > 0", dialogId)); @@ -5939,8 +5982,8 @@ public class MessagesStorage extends BaseController { state.dispose(); } } - cursor.dispose(); } + cursor.dispose(); if (load_type == 3 || load_type == 4 || queryFromServer && load_type == 2) { cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(mid) FROM messages WHERE uid = %d AND mid > 0", dialogId)); @@ -6421,15 +6464,15 @@ public class MessagesStorage extends BaseController { int mentionsUnreadFinal = mentions_unread; int countUnreadFinal = count_unread; int maxUnreadDateFinal = max_unread_date; - if (!scheduled && mergeDialogId != 0 && res.messages.size() < count && isEnd && load_type == 2) { + /*if (!scheduled && mergeDialogId != 0 && res.messages.size() < count && isEnd && load_type == 2) { TODO fix if end not reached Runnable runnable = getMessagesInternal(mergeDialogId, 0, count, Integer.MAX_VALUE, 0, 0, classGuid, 0, false, false, loadIndex); return () -> { getMessagesController().processLoadedMessages(res, dialogId, mergeDialogId, countQueryFinal, maxIdOverrideFinal, offset_date, true, classGuid, minUnreadIdFinal, lastMessageIdFinal, countUnreadFinal, maxUnreadDateFinal, load_type, isChannel, isEndFinal, scheduled, -loadIndex, queryFromServerFinal, mentionsUnreadFinal); runnable.run(); }; - } else { + } else {*/ return () -> getMessagesController().processLoadedMessages(res, dialogId, mergeDialogId, countQueryFinal, maxIdOverrideFinal, offset_date, true, classGuid, minUnreadIdFinal, lastMessageIdFinal, countUnreadFinal, maxUnreadDateFinal, load_type, isChannel, isEndFinal, scheduled, loadIndex, queryFromServerFinal, mentionsUnreadFinal); - } + //} } public void getMessages(long dialogId, long mergeDialogId, boolean loadInfo, int count, int max_id, int offset_date, int minDate, int classGuid, int load_type, boolean isChannel, boolean scheduled, int loadIndex) { @@ -7172,6 +7215,32 @@ public class MessagesStorage extends BaseController { }); } + private void deleteFromDownloadQueue(ArrayList> ids, boolean transaction) { + if (ids == null || ids.isEmpty()) { + return; + } + try { + if (transaction) { + database.beginTransaction(); + } + SQLitePreparedStatement state = database.executeFast("DELETE FROM download_queue WHERE uid = ? AND type = ?"); + for (int a = 0, N = ids.size(); a < N; a++) { + Pair pair = ids.get(a); + state.requery(); + state.bindLong(1, pair.first); + state.bindInteger(1, pair.second); + state.step(); + } + state.dispose(); + if (transaction) { + database.commitTransaction(); + } + AndroidUtilities.runOnUIThread(() -> getDownloadController().cancelDownloading(ids)); + } catch (Exception e) { + FileLog.e(e); + } + } + public void clearDownloadQueue(final int type) { storageQueue.postRunnable(() -> { try { @@ -7934,6 +8003,7 @@ public class MessagesStorage extends BaseController { state_dialogs_replace.bindInteger(13, 0); state_dialogs_replace.bindNull(14); state_dialogs_replace.step(); + unknownDialogsIds.put(key, true); } } state_dialogs_update.dispose(); @@ -8097,8 +8167,8 @@ public class MessagesStorage extends BaseController { if (did == 0) { return null; } + SQLitePreparedStatement state = null; if (oldMessageId == newMessageId && date != 0) { - SQLitePreparedStatement state = null; try { if (scheduled == 0) { state = database.executeFast("UPDATE messages SET send_state = 0, date = ? WHERE mid = ?"); @@ -8115,11 +8185,8 @@ public class MessagesStorage extends BaseController { state.dispose(); } } - return new long[]{did, newId}; } else { - SQLitePreparedStatement state = null; - if (scheduled == 0) { try { state = database.executeFast("UPDATE messages SET mid = ?, send_state = 0 WHERE mid = ?"); @@ -8464,6 +8531,7 @@ public class MessagesStorage extends BaseController { ids = TextUtils.join(",", messages); } ArrayList filesToDelete = new ArrayList<>(); + ArrayList> idsToDelete = new ArrayList<>(); int currentUser = getUserConfig().getClientUserId(); SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT uid, data, read_state, out, mention, mid FROM messages WHERE mid IN(%s)", ids)); @@ -8496,14 +8564,14 @@ public class MessagesStorage extends BaseController { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); message.readAttachPath(data, getUserConfig().clientUserId); data.reuse(); - addFilesToDelete(message, filesToDelete, false); + addFilesToDelete(message, filesToDelete, idsToDelete, false); } } } catch (Exception e) { FileLog.e(e); } cursor.dispose(); - + deleteFromDownloadQueue(idsToDelete, true); getFileLoader().deleteFiles(filesToDelete, 0); for (int a = 0; a < dialogsToUpdate.size(); a++) { @@ -8782,6 +8850,7 @@ public class MessagesStorage extends BaseController { maxMessageId |= ((long) channelId) << 32; ArrayList filesToDelete = new ArrayList<>(); + ArrayList> idsToDelete = new ArrayList<>(); int currentUser = getUserConfig().getClientUserId(); SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT uid, data, read_state, out, mention FROM messages WHERE uid = %d AND mid <= %d", -channelId, maxMessageId)); @@ -8813,7 +8882,7 @@ public class MessagesStorage extends BaseController { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); message.readAttachPath(data, getUserConfig().clientUserId); data.reuse(); - addFilesToDelete(message, filesToDelete, false); + addFilesToDelete(message, filesToDelete, idsToDelete, false); } } } catch (Exception e) { @@ -8821,6 +8890,7 @@ public class MessagesStorage extends BaseController { } cursor.dispose(); + deleteFromDownloadQueue(idsToDelete, true); getFileLoader().deleteFiles(filesToDelete, 0); for (int a = 0; a < dialogsToUpdate.size(); a++) { @@ -9253,6 +9323,7 @@ public class MessagesStorage extends BaseController { //load_type == 3 ? load around message //load_type == 4 ? load around date ArrayList filesToDelete = new ArrayList<>(); + ArrayList> idsToDelete = new ArrayList<>(); SQLitePreparedStatement state_messages = database.executeFast("REPLACE INTO messages VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)"); SQLitePreparedStatement state_media = database.executeFast("REPLACE INTO media_v2 VALUES(?, ?, ?, ?, ?)"); @@ -9292,7 +9363,7 @@ public class MessagesStorage extends BaseController { sameMedia = oldMessage.media.document.id == message.media.document.id; } if (!sameMedia) { - addFilesToDelete(oldMessage, filesToDelete, false); + addFilesToDelete(oldMessage, filesToDelete, idsToDelete, false); } } boolean oldMention = cursor.intValue(3) != 0; @@ -9359,8 +9430,9 @@ public class MessagesStorage extends BaseController { state3.bindInteger(10, message.date); state3.bindInteger(11, pinned); state3.bindInteger(12, flags); - state3.bindInteger(13, 0); + state3.bindInteger(13, -1); state3.bindNull(14); + unknownDialogsIds.put(dialog_id, true); } state3.step(); state3.dispose(); @@ -9447,6 +9519,7 @@ public class MessagesStorage extends BaseController { if (botKeyboard != null) { getMediaDataController().putBotKeyboard(dialog_id, botKeyboard); } + deleteFromDownloadQueue(idsToDelete, false); getFileLoader().deleteFiles(filesToDelete, 0); putUsersInternal(messages.users); putChatsInternal(messages.chats); @@ -9855,9 +9928,10 @@ public class MessagesStorage extends BaseController { for (int a = 0; a < dialogs.dialogs.size(); a++) { TLRPC.Dialog dialog = dialogs.dialogs.get(a); - boolean exists = false; DialogObject.initDialog(dialog); + unknownDialogsIds.remove(dialog.id); + if (check == 1) { SQLiteCursor cursor = database.queryFinalized("SELECT did FROM dialogs WHERE did = " + dialog.id); exists = cursor.next(); @@ -10014,13 +10088,17 @@ public class MessagesStorage extends BaseController { storageQueue.postRunnable(() -> { try { int folderId; - SQLiteCursor cursor = database.queryFinalized("SELECT folder_id FROM dialogs WHERE did = ?", dialogId); - if (cursor.next()) { - folderId = cursor.intValue(0); - } else { + if (unknownDialogsIds.get(dialogId) != null) { folderId = -1; + } else { + SQLiteCursor cursor = database.queryFinalized("SELECT folder_id FROM dialogs WHERE did = ?", dialogId); + if (cursor.next()) { + folderId = cursor.intValue(0); + } else { + folderId = -1; + } + cursor.dispose(); } - cursor.dispose(); AndroidUtilities.runOnUIThread(() -> callback.run(folderId)); } catch (Exception e) { FileLog.e(e); @@ -10045,6 +10123,7 @@ public class MessagesStorage extends BaseController { state.bindInteger(2, 0); state.bindLong(3, did); state.step(); + unknownDialogsIds.remove(did); } } else if (inputPeers != null) { for (int a = 0, N = inputPeers.size(); a < N; a++) { @@ -10055,6 +10134,7 @@ public class MessagesStorage extends BaseController { state.bindInteger(2, 0); state.bindLong(3, did); state.step(); + unknownDialogsIds.remove(did); } } else { state.requery(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java b/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java index eb4e1983d..386149c03 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MusicBrowserService.java @@ -393,7 +393,7 @@ public class MusicBrowserService extends MediaBrowserService implements Notifica } lastSelectedDialog = did; MessagesController.getNotificationsSettings(currentAccount).edit().putInt("auto_lastSelectedDialog", did).commit(); - MediaController.getInstance().setPlaylist(arrayList, arrayList.get(id), false); + MediaController.getInstance().setPlaylist(arrayList, arrayList.get(id), 0, false); mediaSession.setQueue(arrayList1); if (did > 0) { TLRPC.User user = users.get(did); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java index fa621622d..4db885cc3 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java @@ -276,19 +276,23 @@ public class MusicPlayerService extends Service implements NotificationCenter.No bldr.setLargeIcon(albumArtPlaceholder); } + final String nextDescription = LocaleController.getString("Next", R.string.Next); + final String previousDescription = LocaleController.getString("AccDescrPrevious", R.string.AccDescrPrevious); + if (MediaController.getInstance().isDownloadingCurrentMessage()) { playbackState.setState(PlaybackState.STATE_BUFFERING, 0, 1).setActions(0); - bldr.addAction(new Notification.Action.Builder(R.drawable.ic_action_previous, "", pendingPrev).build()) - .addAction(new Notification.Action.Builder(R.drawable.loading_animation2, "", null).build()) - .addAction(new Notification.Action.Builder(R.drawable.ic_action_next, "", pendingNext).build()); + bldr.addAction(new Notification.Action.Builder(R.drawable.ic_action_previous, previousDescription, pendingPrev).build()) + .addAction(new Notification.Action.Builder(R.drawable.loading_animation2, LocaleController.getString("Loading", R.string.Loading), null).build()) + .addAction(new Notification.Action.Builder(R.drawable.ic_action_next, nextDescription, pendingNext).build()); } else { playbackState.setState(isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, MediaController.getInstance().getPlayingMessageObject().audioProgressSec * 1000L, isPlaying ? 1 : 0) .setActions(PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SEEK_TO | PlaybackState.ACTION_SKIP_TO_PREVIOUS | PlaybackState.ACTION_SKIP_TO_NEXT); - bldr.addAction(new Notification.Action.Builder(R.drawable.ic_action_previous, "", pendingPrev).build()) - .addAction(new Notification.Action.Builder(isPlaying ? R.drawable.ic_action_pause : R.drawable.ic_action_play, "", pendingPlaypause).build()) - .addAction(new Notification.Action.Builder(R.drawable.ic_action_next, "", pendingNext).build()); + final String playPauseTitle = isPlaying ? LocaleController.getString("AccActionPause", R.string.AccActionPause) : LocaleController.getString("AccActionPlay", R.string.AccActionPlay); + bldr.addAction(new Notification.Action.Builder(R.drawable.ic_action_previous, previousDescription, pendingPrev).build()) + .addAction(new Notification.Action.Builder(isPlaying ? R.drawable.ic_action_pause : R.drawable.ic_action_play, playPauseTitle, pendingPlaypause).build()) + .addAction(new Notification.Action.Builder(R.drawable.ic_action_next, nextDescription, pendingNext).build()); } mediaSession.setPlaybackState(playbackState.build()); @@ -396,7 +400,6 @@ public class MusicPlayerService extends Service implements NotificationCenter.No } notification.flags |= Notification.FLAG_ONGOING_EVENT; startForeground(ID_NOTIFICATION, notification); - } if (remoteControlClient != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java index f21b32273..fa0db34ab 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java @@ -18,7 +18,7 @@ import tw.nekomimi.nekogram.utils.FileUtil; public class NativeLoader { - private final static int LIB_VERSION = 30; + private final static int LIB_VERSION = 31; private final static String LIB_NAME = "tmessages." + LIB_VERSION; private final static String LIB_SO_NAME = "lib" + LIB_NAME + ".so"; private final static String LOCALE_LIB_SO_NAME = "lib" + LIB_NAME + "loc.so"; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java index ab27d3b37..1e134e42d 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java @@ -44,6 +44,7 @@ public class NotificationCenter { public static final int messagesReadEncrypted = totalEvents++; public static final int encryptedChatCreated = totalEvents++; public static final int dialogPhotosLoaded = totalEvents++; + public static final int reloadDialogPhotos = totalEvents++; public static final int folderBecomeEmpty = totalEvents++; public static final int removeAllMessagesFromDialog = totalEvents++; public static final int notificationsSettingsUpdated = totalEvents++; @@ -74,6 +75,7 @@ public class NotificationCenter { public static final int chatSearchResultsAvailable = totalEvents++; public static final int chatSearchResultsLoading = totalEvents++; public static final int musicDidLoad = totalEvents++; + public static final int moreMusicDidLoad = totalEvents++; public static final int needShowAlert = totalEvents++; public static final int needShowPlayServicesAlert = totalEvents++; public static final int didUpdateMessagesViews = totalEvents++; @@ -100,6 +102,7 @@ public class NotificationCenter { public static final int didUpdateReactions = totalEvents++; public static final int didVerifyMessagesStickers = totalEvents++; public static final int scheduledMessagesUpdated = totalEvents++; + public static final int newSuggestionsAvailable = totalEvents++; public static final int walletPendingTransactionsChanged = totalEvents++; public static final int walletSyncProgressChanged = totalEvents++; @@ -198,6 +201,8 @@ public class NotificationCenter { private SparseArray> removeAfterBroadcast = new SparseArray<>(); private SparseArray> addAfterBroadcast = new SparseArray<>(); private ArrayList delayedPosts = new ArrayList<>(10); + private ArrayList delayedPostsTmp = new ArrayList<>(10); + private ArrayList postponeCallbackList = new ArrayList<>(10); private int broadcasting = 0; @@ -291,17 +296,24 @@ public class NotificationCenter { animationInProgressCount--; if (animationInProgressCount == 0) { NotificationCenter.getGlobalInstance().postNotificationName(startAllHeavyOperations, 512); - if (!delayedPosts.isEmpty()) { - for (int a = 0; a < delayedPosts.size(); a++) { - DelayedPost delayedPost = delayedPosts.get(a); - postNotificationNameInternal(delayedPost.id, true, delayedPost.args); - } - delayedPosts.clear(); - } + runDelayedNotifications(); } } } + public void runDelayedNotifications() { + if (!delayedPosts.isEmpty()) { + delayedPostsTmp.clear(); + delayedPostsTmp.addAll(delayedPosts); + delayedPosts.clear(); + for (int a = 0; a < delayedPostsTmp.size(); a++) { + DelayedPost delayedPost = delayedPostsTmp.get(a); + postNotificationNameInternal(delayedPost.id, true, delayedPost.args); + } + delayedPostsTmp.clear(); + } + } + public boolean isAnimationInProgress() { return animationInProgressCount > 0; } @@ -311,7 +323,7 @@ public class NotificationCenter { } public void postNotificationName(int id, Object... args) { - boolean allowDuringAnimation = id == startAllHeavyOperations || id == stopAllHeavyOperations; + boolean allowDuringAnimation = id == startAllHeavyOperations || id == stopAllHeavyOperations || id == didReplacedPhotoInMemCache; if (!allowDuringAnimation && !allowedNotifications.isEmpty()) { int size = allowedNotifications.size(); int allowedCount = 0; @@ -355,6 +367,14 @@ public class NotificationCenter { } return; } + if (!postponeCallbackList.isEmpty()) { + for (int i = 0; i < postponeCallbackList.size(); i++) { + if (postponeCallbackList.get(i).needPostpone(id, currentAccount, args)) { + delayedPosts.add(new DelayedPost(id, args)); + return; + } + } + } broadcasting++; ArrayList objects = observers.get(id); if (objects != null && !objects.isEmpty()) { @@ -437,4 +457,30 @@ public class NotificationCenter { public boolean hasObservers(int id) { return observers.indexOfKey(id) >= 0; } + + public void addPostponeNotificationsCallback(PostponeNotificationCallback callback) { + if (BuildVars.DEBUG_VERSION) { + if (Thread.currentThread() != ApplicationLoader.applicationHandler.getLooper().getThread()) { + throw new RuntimeException("PostponeNotificationsCallback allowed only from MAIN thread"); + } + } + if (!postponeCallbackList.contains(callback)) { + postponeCallbackList.add(callback); + } + } + + public void removePostponeNotificationsCallback(PostponeNotificationCallback callback) { + if (BuildVars.DEBUG_VERSION) { + if (Thread.currentThread() != ApplicationLoader.applicationHandler.getLooper().getThread()) { + throw new RuntimeException("removePostponeNotificationsCallback allowed only from MAIN thread"); + } + } + if (postponeCallbackList.remove(callback)) { + runDelayedNotifications(); + } + } + + public interface PostponeNotificationCallback { + boolean needPostpone(int id, int currentAccount, Object[] args); + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java index d122e3dcf..0dbe1e41b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java @@ -45,6 +45,8 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.app.RemoteInput; import androidx.core.content.FileProvider; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import android.text.TextUtils; import android.util.LongSparseArray; @@ -57,12 +59,14 @@ import org.json.JSONObject; import org.telegram.messenger.support.SparseLongArray; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; +import org.telegram.ui.BubbleActivity; import org.telegram.ui.LaunchActivity; import org.telegram.ui.PopupNotificationActivity; import java.io.File; import java.util.ArrayList; import java.util.Calendar; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -86,6 +90,7 @@ public class NotificationsController extends BaseController { private LongSparseArray pushDialogsOverrideMention = new LongSparseArray<>(); public ArrayList popupMessages = new ArrayList<>(); public ArrayList popupReplyMessages = new ArrayList<>(); + private HashSet openedInBubbleDialogs = new HashSet<>(); private long opened_dialog_id = 0; private int lastButtonId = 5000; private int total_unread_count = 0; @@ -222,7 +227,11 @@ public class NotificationsController extends BaseController { notificationChannel.enableLights(false); notificationChannel.enableVibration(false); notificationChannel.setSound(null, null); - systemNotificationManager.createNotificationChannel(notificationChannel); + try { + systemNotificationManager.createNotificationChannel(notificationChannel); + } catch (Exception e) { + FileLog.e(e); + } } } @@ -239,6 +248,7 @@ public class NotificationsController extends BaseController { pushDialogs.clear(); wearNotificationsIds.clear(); lastWearNotifiedMessageId.clear(); + openedInBubbleDialogs.clear(); delayedPushMessages.clear(); notifyCheck = false; lastBadgeCount = 0; @@ -295,6 +305,16 @@ public class NotificationsController extends BaseController { notificationsQueue.postRunnable(() -> opened_dialog_id = dialog_id); } + public void setOpenedInBubble(final long dialog_id, boolean opened) { + notificationsQueue.postRunnable(() -> { + if (opened) { + openedInBubbleDialogs.add(dialog_id); + } else { + openedInBubbleDialogs.remove(dialog_id); + } + }); + } + public void setLastOnlineFromOtherDevice(final int time) { notificationsQueue.postRunnable(() -> { if (BuildVars.LOGS_ENABLED) { @@ -401,6 +421,7 @@ public class NotificationsController extends BaseController { for (int a = 0, size = popupArrayRemove.size(); a < size; a++) { popupMessages.remove(popupArrayRemove.get(a)); } + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.pushMessagesUpdated); }); } if (old_unread_count != total_unread_count) { @@ -473,6 +494,7 @@ public class NotificationsController extends BaseController { for (int a = 0, size = popupArrayRemove.size(); a < size; a++) { popupMessages.remove(popupArrayRemove.get(a)); } + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.pushMessagesUpdated); }); } if (old_unread_count != total_unread_count) { @@ -772,7 +794,7 @@ public class NotificationsController extends BaseController { } Integer currentCount = pushDialogs.get(dialog_id); - Integer newCount = currentCount != null ? currentCount + 1 : 1; + int newCount = currentCount != null ? currentCount + 1 : 1; if (notifyCheck && !canAddValue) { Integer override = pushDialogsOverrideMention.get(dialog_id); @@ -885,6 +907,7 @@ public class NotificationsController extends BaseController { for (int a = 0, size = popupArrayToRemove.size(); a < size; a++) { popupMessages.remove(popupArrayToRemove.get(a)); } + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.pushMessagesUpdated); }); } if (old_unread_count != total_unread_count) { @@ -1046,7 +1069,7 @@ public class NotificationsController extends BaseController { } Integer currentCount = pushDialogs.get(dialog_id); - Integer newCount = currentCount != null ? currentCount + 1 : 1; + int newCount = currentCount != null ? currentCount + 1 : 1; if (currentCount != null) { total_unread_count -= currentCount; @@ -1293,9 +1316,17 @@ public class NotificationsController extends BaseController { return LocaleController.formatString("NotificationEditedGroupName", R.string.NotificationEditedGroupName, name, messageObject.messageOwner.action.title); } else if (messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatEditPhoto || messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatDeletePhoto) { if (messageObject.messageOwner.to_id.channel_id != 0 && !chat.megagroup) { - return LocaleController.formatString("ChannelPhotoEditNotification", R.string.ChannelPhotoEditNotification, chat.title); + if (messageObject.isVideoAvatar()) { + return LocaleController.formatString("ChannelVideoEditNotification", R.string.ChannelVideoEditNotification, chat.title); + } else { + return LocaleController.formatString("ChannelPhotoEditNotification", R.string.ChannelPhotoEditNotification, chat.title); + } } else { - return LocaleController.formatString("NotificationEditedGroupPhoto", R.string.NotificationEditedGroupPhoto, name, chat.title); + if (messageObject.isVideoAvatar()) { + return LocaleController.formatString("NotificationEditedGroupVideo", R.string.NotificationEditedGroupVideo, name, chat.title); + } else { + return LocaleController.formatString("NotificationEditedGroupPhoto", R.string.NotificationEditedGroupPhoto, name, chat.title); + } } } else if (messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatDeleteUser) { if (messageObject.messageOwner.action.user_id == getUserConfig().getClientUserId()) { @@ -1796,9 +1827,17 @@ public class NotificationsController extends BaseController { msg = LocaleController.formatString("NotificationEditedGroupName", R.string.NotificationEditedGroupName, name, messageObject.messageOwner.action.title); } else if (messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatEditPhoto || messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatDeletePhoto) { if (messageObject.messageOwner.to_id.channel_id != 0 && !chat.megagroup) { - msg = LocaleController.formatString("ChannelPhotoEditNotification", R.string.ChannelPhotoEditNotification, chat.title); + if (messageObject.isVideoAvatar()) { + msg = LocaleController.formatString("ChannelVideoEditNotification", R.string.ChannelVideoEditNotification, chat.title); + } else { + msg = LocaleController.formatString("ChannelPhotoEditNotification", R.string.ChannelPhotoEditNotification, chat.title); + } } else { - msg = LocaleController.formatString("NotificationEditedGroupPhoto", R.string.NotificationEditedGroupPhoto, name, chat.title); + if (messageObject.isVideoAvatar()) { + msg = LocaleController.formatString("NotificationEditedGroupVideo", R.string.NotificationEditedGroupVideo, name, chat.title); + } else { + msg = LocaleController.formatString("NotificationEditedGroupPhoto", R.string.NotificationEditedGroupPhoto, name, chat.title); + } } } else if (messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatDeleteUser) { if (messageObject.messageOwner.action.user_id == selfUsedId) { @@ -2187,6 +2226,10 @@ public class NotificationsController extends BaseController { pushMessagesDict.clear(); lastWearNotifiedMessageId.clear(); for (int a = 0; a < wearNotificationsIds.size(); a++) { + long did = wearNotificationsIds.keyAt(a); + if (openedInBubbleDialogs.contains(did)) { + continue; + } notificationManager.cancel(wearNotificationsIds.valueAt(a)); } wearNotificationsIds.clear(); @@ -2335,6 +2378,62 @@ public class NotificationsController extends BaseController { }); } + @SuppressLint("RestrictedApi") + private void createNotificationShortcut(NotificationCompat.Builder builder, int did, String name, TLRPC.User user, TLRPC.Chat chat, Person person) { + if (Build.VERSION.SDK_INT < 29 || ChatObject.isChannel(chat) && !chat.megagroup || !SharedConfig.chatBubbles) { + return; + } + try { + String id = "ndid_" + did; + ShortcutInfoCompat.Builder shortcutBuilder = new ShortcutInfoCompat.Builder(ApplicationLoader.applicationContext, id) + .setShortLabel(chat != null ? name : UserObject.getFirstName(user)) + .setLongLabel(name) + .setIntent(new Intent(Intent.ACTION_DEFAULT)) + .setLongLived(true); + + Bitmap avatar = null; + if (person != null) { + shortcutBuilder.setPerson(person); + shortcutBuilder.setIcon(person.getIcon()); + if (person.getIcon() != null) { + avatar = person.getIcon().getBitmap(); + } + } + ArrayList arrayList = new ArrayList<>(1); + arrayList.add(shortcutBuilder.build()); + ArrayList ids = new ArrayList<>(1); + ids.add(id); + ShortcutManagerCompat.addDynamicShortcuts(ApplicationLoader.applicationContext, arrayList); + ShortcutManagerCompat.removeDynamicShortcuts(ApplicationLoader.applicationContext, ids); + builder.setShortcutId(id); + NotificationCompat.BubbleMetadata.Builder bubbleBuilder = new NotificationCompat.BubbleMetadata.Builder(); + Intent intent = new Intent(ApplicationLoader.applicationContext, BubbleActivity.class); + intent.setAction("com.tmessages.openchat" + Math.random() + Integer.MAX_VALUE); + if (did > 0) { + intent.putExtra("userId", did); + } else { + intent.putExtra("chatId", -did); + } + intent.putExtra("currentAccount", currentAccount); + bubbleBuilder.setIntent(PendingIntent.getActivity(ApplicationLoader.applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)); + bubbleBuilder.setSuppressNotification(true); + bubbleBuilder.setAutoExpandBubble(false); + bubbleBuilder.setDesiredHeight(AndroidUtilities.dp(640)); + if (avatar != null) { + bubbleBuilder.setIcon(IconCompat.createWithAdaptiveBitmap(avatar)); + } else { + if (user != null) { + bubbleBuilder.setIcon(IconCompat.createWithResource(ApplicationLoader.applicationContext, user.bot ? R.drawable.book_bot : R.drawable.book_user)); + } else { + bubbleBuilder.setIcon(IconCompat.createWithResource(ApplicationLoader.applicationContext, R.drawable.book_group)); + } + } + builder.setBubbleMetadata(bubbleBuilder.build()); + } catch (Exception e) { + FileLog.e(e); + } + } + @TargetApi(26) private String validateChannelId(long dialogId, String name, long[] vibrationPattern, int ledColor, Uri sound, int importance, long[] configVibrationPattern, Uri configSound, int configImportance) { SharedPreferences preferences = getAccountInstance().getNotificationsSettings(); @@ -3013,6 +3112,15 @@ public class NotificationsController extends BaseController { } } + @SuppressLint("NewApi") + private void setNotificationChannel(Notification mainNotification, NotificationCompat.Builder builder, boolean useSummaryNotification) { + if (useSummaryNotification) { + builder.setChannelId(OTHER_NOTIFICATIONS_CHANNEL); + } else { + builder.setChannelId(mainNotification.getChannelId()); + } + } + @SuppressLint("InlinedApi") private void showExtraNotifications(NotificationCompat.Builder notificationBuilder, boolean notifyAboutLast, String summary) { Notification mainNotification = notificationBuilder.build(); @@ -3377,9 +3485,14 @@ public class NotificationsController extends BaseController { File attach = FileLoader.getPathToMessage(messageObject.messageOwner); NotificationCompat.MessagingStyle.Message msg = new NotificationCompat.MessagingStyle.Message(message, ((long) messageObject.messageOwner.date) * 1000L, person); String mimeType = messageObject.isSticker() ? "image/webp" : "image/jpeg"; - final Uri uri; + Uri uri; if (attach.exists()) { - uri = FileProvider.getUriForFile(ApplicationLoader.applicationContext, BuildConfig.APPLICATION_ID + ".provider", attach); + try { + uri = FileProvider.getUriForFile(ApplicationLoader.applicationContext, BuildConfig.APPLICATION_ID + ".provider", attach); + } catch (Exception e) { + FileLog.e(e); + uri = null; + } } else if (getFileLoader().isLoadingFile(attach.getName())) { Uri.Builder _uri = new Uri.Builder() .scheme("content") @@ -3395,8 +3508,9 @@ public class NotificationsController extends BaseController { if (uri != null) { msg.setData(mimeType, uri); messagingStyle.addMessage(msg); + final Uri uriFinal = uri; ApplicationLoader.applicationContext.grantUriPermission("com.android.systemui", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - AndroidUtilities.runOnUIThread(() -> ApplicationLoader.applicationContext.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION), 20000); + AndroidUtilities.runOnUIThread(() -> ApplicationLoader.applicationContext.revokeUriPermission(uriFinal, Intent.FLAG_GRANT_READ_URI_PERMISSION), 20000); if (!TextUtils.isEmpty(messageObject.caption)) { messagingStyle.addMessage(messageObject.caption, ((long) messageObject.messageOwner.date) * 1000, person); @@ -3520,7 +3634,6 @@ public class NotificationsController extends BaseController { .setGroupSummary(false) .setWhen(date) .setShowWhen(true) - .setShortcutId("sdid_" + dialog_id) .setStyle(messagingStyle) .setContentIntent(contentIntent) .extend(wearableExtender) @@ -3554,23 +3667,30 @@ public class NotificationsController extends BaseController { builder.setLargeIcon(avatarBitmap); } - if (!AndroidUtilities.needShowPasscode(false) && !SharedConfig.isWaitingForPasscodeEnter && rows != null) { - for (int r = 0, rc = rows.size(); r < rc; r++) { - TLRPC.TL_keyboardButtonRow row = rows.get(r); - for (int c = 0, cc = row.buttons.size(); c < cc; c++) { - TLRPC.KeyboardButton button = row.buttons.get(c); - if (button instanceof TLRPC.TL_keyboardButtonCallback) { - Intent callbackIntent = new Intent(ApplicationLoader.applicationContext, NotificationCallbackReceiver.class); - callbackIntent.putExtra("currentAccount", currentAccount); - callbackIntent.putExtra("did", dialog_id); - if (button.data != null) { - callbackIntent.putExtra("data", button.data); + if (!AndroidUtilities.needShowPasscode(false) && !SharedConfig.isWaitingForPasscodeEnter) { + if (rows != null) { + for (int r = 0, rc = rows.size(); r < rc; r++) { + TLRPC.TL_keyboardButtonRow row = rows.get(r); + for (int c = 0, cc = row.buttons.size(); c < cc; c++) { + TLRPC.KeyboardButton button = row.buttons.get(c); + if (button instanceof TLRPC.TL_keyboardButtonCallback) { + Intent callbackIntent = new Intent(ApplicationLoader.applicationContext, NotificationCallbackReceiver.class); + callbackIntent.putExtra("currentAccount", currentAccount); + callbackIntent.putExtra("did", dialog_id); + if (button.data != null) { + callbackIntent.putExtra("data", button.data); + } + callbackIntent.putExtra("mid", rowsMid); + builder.addAction(0, button.text, PendingIntent.getBroadcast(ApplicationLoader.applicationContext, lastButtonId++, callbackIntent, PendingIntent.FLAG_UPDATE_CURRENT)); } - callbackIntent.putExtra("mid", rowsMid); - builder.addAction(0, button.text, PendingIntent.getBroadcast(ApplicationLoader.applicationContext, lastButtonId++, callbackIntent, PendingIntent.FLAG_UPDATE_CURRENT)); } } } + if (Build.VERSION.SDK_INT >= 29) { + if (lowerId != 0) { + createNotificationShortcut(builder, lowerId, name, user, chat, personCache.get(lowerId)); + } + } } if (chat == null && user != null && user.phone != null && user.phone.length() > 0) { @@ -3578,11 +3698,7 @@ public class NotificationsController extends BaseController { } if (Build.VERSION.SDK_INT >= 26) { - if (useSummaryNotification) { - builder.setChannelId(OTHER_NOTIFICATIONS_CHANNEL); - } else { - builder.setChannelId(mainNotification.getChannelId()); - } + setNotificationChannel(mainNotification, builder, useSummaryNotification); } holders.add(new NotificationHolder(internalId, builder.build())); wearNotificationsIds.put(dialog_id, internalId); @@ -3623,16 +3739,22 @@ public class NotificationsController extends BaseController { } notificationManager.notify(notificationId, mainNotification); } else { - notificationManager.cancel(notificationId); + if (openedInBubbleDialogs.isEmpty()) { + notificationManager.cancel(notificationId); + } } for (int a = 0, size = holders.size(); a < size; a++) { holders.get(a).call(); } for (int a = 0; a < oldIdsWear.size(); a++) { + long did = oldIdsWear.keyAt(a); + if (openedInBubbleDialogs.contains(did)) { + continue; + } Integer id = oldIdsWear.valueAt(a); if (BuildVars.LOGS_ENABLED) { - FileLog.w("cancel notification id " + id); + FileLog.d("cancel notification id " + id); } notificationManager.cancel(id); } @@ -3723,6 +3845,19 @@ public class NotificationsController extends BaseController { public static final int SETTING_MUTE_FOREVER = 3; public static final int SETTING_MUTE_UNMUTE = 4; + public void clearDialogNotificationsSettings(long did) { + SharedPreferences preferences = getAccountInstance().getNotificationsSettings(); + SharedPreferences.Editor editor = preferences.edit(); + editor.remove("notify2_" + did).remove("custom_" + did); + getMessagesStorage().setDialogFlags(did, 0); + TLRPC.Dialog dialog = getMessagesController().dialogs_dict.get(did); + if (dialog != null) { + dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); + } + editor.commit(); + getNotificationsController().updateServerNotificationsSettings(did, true); + } + public void setDialogNotificationsSettings(long dialog_id, int setting) { SharedPreferences preferences = getAccountInstance().getNotificationsSettings(); SharedPreferences.Editor editor = preferences.edit(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java index b6589737b..678b13449 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java @@ -10,6 +10,8 @@ package org.telegram.messenger; import android.app.Activity; import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; import android.util.LongSparseArray; import android.util.SparseArray; import android.util.SparseIntArray; @@ -195,11 +197,17 @@ public class SecretChatHelper extends BaseController { newChat.user_id = user_id; final TLRPC.Dialog dialog = new TLRPC.TL_dialog(); dialog.id = dialog_id; + dialog.folder_id = newChat.folder_id; dialog.unread_count = 0; dialog.top_message = 0; dialog.last_message_date = update.date; getMessagesController().putEncryptedChat(newChat, false); AndroidUtilities.runOnUIThread(() -> { + if (dialog.folder_id == 1) { + SharedPreferences.Editor editor = MessagesController.getNotificationsSettings(currentAccount).edit(); + editor.putBoolean("dialog_bar_archived" + dialog_id, true); + editor.commit(); + } getMessagesController().dialogs_dict.put(dialog.id, dialog); getMessagesController().allDialogs.add(dialog); getMessagesController().sortDialogs(null); @@ -463,6 +471,7 @@ public class SecretChatHelper extends BaseController { MessageObject newMsgObj = new MessageObject(currentAccount, message, false); newMsgObj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SENDING; + newMsgObj.wasJustSent = true; ArrayList objArr = new ArrayList<>(); objArr.add(newMsgObj); getMessagesController().updateInterfaceWithMessages(message.dialog_id, objArr, false); @@ -492,6 +501,7 @@ public class SecretChatHelper extends BaseController { MessageObject newMsgObj = new MessageObject(currentAccount, message, false); newMsgObj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SENDING; + newMsgObj.wasJustSent = true; ArrayList objArr = new ArrayList<>(); objArr.add(newMsgObj); getMessagesController().updateInterfaceWithMessages(message.dialog_id, objArr, false); @@ -894,7 +904,9 @@ public class SecretChatHelper extends BaseController { } newMessage.media = new TLRPC.TL_messageMediaPhoto(); newMessage.media.flags |= 3; - newMessage.message = decryptedMessage.media.caption != null ? decryptedMessage.media.caption : ""; + if (TextUtils.isEmpty(newMessage.message)) { + newMessage.message = decryptedMessage.media.caption != null ? decryptedMessage.media.caption : ""; + } newMessage.media.photo = new TLRPC.TL_photo(); newMessage.media.photo.file_reference = new byte[0]; newMessage.media.photo.date = newMessage.date; @@ -936,7 +948,9 @@ public class SecretChatHelper extends BaseController { newMessage.media.document.key = decryptedMessage.media.key; newMessage.media.document.iv = decryptedMessage.media.iv; newMessage.media.document.dc_id = file.dc_id; - newMessage.message = decryptedMessage.media.caption != null ? decryptedMessage.media.caption : ""; + if (TextUtils.isEmpty(newMessage.message)) { + newMessage.message = decryptedMessage.media.caption != null ? decryptedMessage.media.caption : ""; + } newMessage.media.document.date = date; newMessage.media.document.size = file.size; newMessage.media.document.id = file.id; @@ -979,7 +993,9 @@ public class SecretChatHelper extends BaseController { } newMessage.media = new TLRPC.TL_messageMediaDocument(); newMessage.media.flags |= 3; - newMessage.message = decryptedMessage.media.caption != null ? decryptedMessage.media.caption : ""; + if (TextUtils.isEmpty(newMessage.message)) { + newMessage.message = decryptedMessage.media.caption != null ? decryptedMessage.media.caption : ""; + } newMessage.media.document = new TLRPC.TL_documentEncrypted(); newMessage.media.document.id = file.id; newMessage.media.document.access_hash = file.access_hash; @@ -992,6 +1008,16 @@ public class SecretChatHelper extends BaseController { } else { newMessage.media.document.attributes = decryptedMessage.media.attributes; } + if (newMessage.ttl > 0) { + for (int a = 0, N = newMessage.media.document.attributes.size(); a < N; a++) { + TLRPC.DocumentAttribute attribute = newMessage.media.document.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeAudio || attribute instanceof TLRPC.TL_documentAttributeVideo) { + newMessage.ttl = Math.max(attribute.duration + 1, newMessage.ttl); + break; + } + } + newMessage.ttl = Math.max(decryptedMessage.media.duration + 1, newMessage.ttl); + } newMessage.media.document.size = decryptedMessage.media.size != 0 ? Math.min(decryptedMessage.media.size, file.size) : file.size; newMessage.media.document.key = decryptedMessage.media.key; newMessage.media.document.iv = decryptedMessage.media.iv; @@ -1056,7 +1082,9 @@ public class SecretChatHelper extends BaseController { newMessage.media.document.size = file.size; newMessage.media.document.dc_id = file.dc_id; newMessage.media.document.mime_type = decryptedMessage.media.mime_type; - newMessage.message = decryptedMessage.media.caption != null ? decryptedMessage.media.caption : ""; + if (TextUtils.isEmpty(newMessage.message)) { + newMessage.message = decryptedMessage.media.caption != null ? decryptedMessage.media.caption : ""; + } if (newMessage.media.document.mime_type == null) { newMessage.media.document.mime_type = "audio/ogg"; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java index 044e10a50..92bb10998 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java @@ -1092,6 +1092,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe MessageObject newMsgObj = new MessageObject(currentAccount, message, false); newMsgObj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SENDING; + newMsgObj.wasJustSent = true; ArrayList objArr = new ArrayList<>(); objArr.add(newMsgObj); getMessagesController().updateInterfaceWithMessages(message.dialog_id, objArr, false); @@ -1276,13 +1277,27 @@ public class SendMessagesHelper extends BaseController implements NotificationCe boolean forwardFromSaved = msgObj.getDialogId() == myId && msgObj.messageOwner.from_id == getUserConfig().getClientUserId(); if (msgObj.isForwarded()) { newMsg.fwd_from = new TLRPC.TL_messageFwdHeader(); - newMsg.fwd_from.flags = msgObj.messageOwner.fwd_from.flags; - newMsg.fwd_from.from_id = msgObj.messageOwner.fwd_from.from_id; + if ((newMsg.fwd_from.flags & 1) != 0) { + newMsg.fwd_from.flags |= 1; + newMsg.fwd_from.from_id = msgObj.messageOwner.fwd_from.from_id; + } + if ((newMsg.fwd_from.flags & 32) != 0) { + newMsg.fwd_from.flags |= 32; + newMsg.fwd_from.from_name = msgObj.messageOwner.fwd_from.from_name; + } + if ((newMsg.fwd_from.flags & 2) != 0) { + newMsg.fwd_from.flags |= 2; + newMsg.fwd_from.channel_id = msgObj.messageOwner.fwd_from.channel_id; + } + if ((newMsg.fwd_from.flags & 4) != 0) { + newMsg.fwd_from.flags |= 4; + newMsg.fwd_from.channel_post = msgObj.messageOwner.fwd_from.channel_post; + } + if ((newMsg.fwd_from.flags & 8) != 0) { + newMsg.fwd_from.flags |= 8; + newMsg.fwd_from.post_author = msgObj.messageOwner.fwd_from.post_author; + } newMsg.fwd_from.date = msgObj.messageOwner.fwd_from.date; - newMsg.fwd_from.channel_id = msgObj.messageOwner.fwd_from.channel_id; - newMsg.fwd_from.channel_post = msgObj.messageOwner.fwd_from.channel_post; - newMsg.fwd_from.post_author = msgObj.messageOwner.fwd_from.post_author; - newMsg.fwd_from.from_name = msgObj.messageOwner.fwd_from.from_name; newMsg.flags = TLRPC.MESSAGE_FLAG_FWD; } else if (!forwardFromSaved) { //if (!toMyself || !msgObj.isOutOwner()) newMsg.fwd_from = new TLRPC.TL_messageFwdHeader(); @@ -1446,6 +1461,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe MessageObject newMsgObj = new MessageObject(currentAccount, newMsg, true); newMsgObj.scheduled = scheduleDate != 0; newMsgObj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SENDING; + newMsgObj.wasJustSent = true; objArr.add(newMsgObj); arr.add(newMsg); @@ -1485,7 +1501,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe req.with_my_score = messages.size() == 1 && messages.get(0).messageOwner.with_my_score; final ArrayList newMsgObjArr = arr; - final ArrayList newMsgArr = objArr; + final ArrayList newMsgArr = new ArrayList<>(objArr); final LongSparseArray messagesByRandomIdsFinal = messagesByRandomIds; final boolean isMegagroupFinal = isMegagroup; boolean scheduledOnline = scheduleDate == 0x7FFFFFFE; @@ -2496,7 +2512,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe webPage = null; } } - if (webPage == null && (entities == null || entities.isEmpty()) && getMessagesController().diceEmojies.contains(message) && encryptedChat == null && scheduleDate == 0) { + if (message.length() < 30 && webPage == null && (entities == null || entities.isEmpty()) && getMessagesController().diceEmojies.contains(message.replace("\ufe0f", "")) && encryptedChat == null && scheduleDate == 0) { TLRPC.TL_messageMediaDice mediaDice = new TLRPC.TL_messageMediaDice(); mediaDice.emoticon = message; mediaDice.value = -1; @@ -2813,6 +2829,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe newMsg.send_state = MessageObject.MESSAGE_SEND_STATE_SENDING; newMsgObj = new MessageObject(currentAccount, newMsg, reply_to_msg, true); + newMsgObj.wasJustSent = true; newMsgObj.scheduled = scheduleDate != 0; if (!newMsgObj.isForwarded() && (newMsgObj.type == 3 || videoEditedInfo != null || newMsgObj.type == 2) && !TextUtils.isEmpty(newMsg.attachPath)) { newMsgObj.attachPathExists = true; @@ -4041,7 +4058,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe message.sendDelayedRequests(); } - protected void stopVideoService(final String path) { + public void stopVideoService(final String path) { getMessagesStorage().getStorageQueue().postRunnable(() -> AndroidUtilities.runOnUIThread(() -> NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.stopEncodingService, path, currentAccount))); } @@ -4651,7 +4668,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe if (sentMessage != null) { newKey = "stripped" + FileRefController.getKeyForParentObject(sentMessage); } else { - newKey = "stripped" + "message" + newMsgId + "_" + newMsgObj.getChannelId(); + newKey = "stripped" + "message" + newMsgId + "_" + newMsgObj.getChannelId() + "_" + newMsgObj.scheduled; } ImageLoader.getInstance().replaceImageInCache(oldKey, newKey, ImageLocation.getForObject(strippedNew, photoObject), post); } @@ -4704,7 +4721,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe newMsg.media.photo.id = sentMessage.media.photo.id; newMsg.media.photo.access_hash = sentMessage.media.photo.access_hash; } else if (sentMessage.media instanceof TLRPC.TL_messageMediaDocument && sentMessage.media.document != null && newMsg.media instanceof TLRPC.TL_messageMediaDocument && newMsg.media.document != null) { - if (sentMessage.media.ttl_seconds == 0 && (newMsgObj.videoEditedInfo == null || newMsgObj.videoEditedInfo.mediaEntities == null && TextUtils.isEmpty(newMsgObj.videoEditedInfo.paintPath))) { + if (sentMessage.media.ttl_seconds == 0 && (newMsgObj.videoEditedInfo == null || newMsgObj.videoEditedInfo.mediaEntities == null && TextUtils.isEmpty(newMsgObj.videoEditedInfo.paintPath) && newMsgObj.videoEditedInfo.cropState == null)) { boolean isVideo = MessageObject.isVideoMessage(sentMessage); if ((isVideo || MessageObject.isGifMessage(sentMessage)) && MessageObject.isGifDocument(sentMessage.media.document) == MessageObject.isGifDocument(newMsg.media.document)) { if (!newMsgObj.scheduled) { @@ -5016,13 +5033,13 @@ public class SendMessagesHelper extends BaseController implements NotificationCe String parentObject = null; if (!sendNew && !isEncrypted) { Object[] sentData = accountInstance.getMessagesStorage().getSentFile(originalPath, !isEncrypted ? 1 : 4); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_document) { document = (TLRPC.TL_document) sentData[0]; parentObject = (String) sentData[1]; } if (document == null && !path.equals(originalPath) && !isEncrypted) { sentData = accountInstance.getMessagesStorage().getSentFile(path + f.length(), !isEncrypted ? 1 : 4); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_document) { document = (TLRPC.TL_document) sentData[0]; parentObject = (String) sentData[1]; } @@ -5177,7 +5194,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe String parentObject = null; if (!isEncrypted) { Object[] sentData = accountInstance.getMessagesStorage().getSentFile(originalPath, !isEncrypted ? 1 : 4); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_document) { document = (TLRPC.TL_document) sentData[0]; parentObject = (String) sentData[1]; ensureMediaThumbExists(isEncrypted, document, originalPath, null, 0); @@ -5805,13 +5822,13 @@ public class SendMessagesHelper extends BaseController implements NotificationCe String parentObject = null; if (!isEncrypted && info.ttl == 0) { Object[] sentData = accountInstance.getMessagesStorage().getSentFile(originalPath, !isEncrypted ? 0 : 3); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_photo) { photo = (TLRPC.TL_photo) sentData[0]; parentObject = (String) sentData[1]; } if (photo == null && info.uri != null) { sentData = accountInstance.getMessagesStorage().getSentFile(AndroidUtilities.getPath(info.uri), !isEncrypted ? 0 : 3); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_photo) { photo = (TLRPC.TL_photo) sentData[0]; parentObject = (String) sentData[1]; } @@ -6065,9 +6082,9 @@ public class SendMessagesHelper extends BaseController implements NotificationCe } TLRPC.TL_document document = null; String parentObject = null; - if (!isEncrypted && info.ttl == 0 && (videoEditedInfo == null || videoEditedInfo.filterState == null && videoEditedInfo.paintPath == null && videoEditedInfo.mediaEntities == null)) { + if (!isEncrypted && info.ttl == 0 && (videoEditedInfo == null || videoEditedInfo.filterState == null && videoEditedInfo.paintPath == null && videoEditedInfo.mediaEntities == null && videoEditedInfo.cropState == null)) { Object[] sentData = accountInstance.getMessagesStorage().getSentFile(originalPath, !isEncrypted ? 2 : 5); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_document) { document = (TLRPC.TL_document) sentData[0]; parentObject = (String) sentData[1]; ensureMediaThumbExists(isEncrypted, document, info.path, null, startTime); @@ -6118,12 +6135,21 @@ public class SendMessagesHelper extends BaseController implements NotificationCe } else { attributeVideo.duration = (int) (videoEditedInfo.estimatedDuration / 1000); } - if (videoEditedInfo.rotationValue == 90 || videoEditedInfo.rotationValue == 270) { - attributeVideo.w = videoEditedInfo.resultHeight; - attributeVideo.h = videoEditedInfo.resultWidth; + int w, h; + int rotation = videoEditedInfo.rotationValue; + if (videoEditedInfo.cropState != null) { + w = videoEditedInfo.cropState.transformWidth; + h = videoEditedInfo.cropState.transformHeight; } else { - attributeVideo.w = videoEditedInfo.resultWidth; - attributeVideo.h = videoEditedInfo.resultHeight; + w = videoEditedInfo.resultWidth; + h = videoEditedInfo.resultHeight; + } + if (rotation == 90 || rotation == 270) { + attributeVideo.w = h; + attributeVideo.h = w; + } else { + attributeVideo.w = w; + attributeVideo.h = h; } document.size = (int) videoEditedInfo.estimatedSize; } else { @@ -6302,13 +6328,13 @@ public class SendMessagesHelper extends BaseController implements NotificationCe } else { if (!isEncrypted && info.ttl == 0) { Object[] sentData = accountInstance.getMessagesStorage().getSentFile(originalPath, !isEncrypted ? 0 : 3); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_photo) { photo = (TLRPC.TL_photo) sentData[0]; parentObject = (String) sentData[1]; } if (photo == null && info.uri != null) { sentData = accountInstance.getMessagesStorage().getSentFile(AndroidUtilities.getPath(info.uri), !isEncrypted ? 0 : 3); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_photo) { photo = (TLRPC.TL_photo) sentData[0]; parentObject = (String) sentData[1]; } @@ -6508,21 +6534,34 @@ public class SendMessagesHelper extends BaseController implements NotificationCe } public static Bitmap createVideoThumbnailAtTime(String filePath, long time) { + return createVideoThumbnailAtTime(filePath, time, null, false); + } + + public static Bitmap createVideoThumbnailAtTime(String filePath, long time, int[] orientation, boolean precise) { Bitmap bitmap = null; - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - try { - retriever.setDataSource(filePath); - bitmap = retriever.getFrameAtTime(time, MediaMetadataRetriever.OPTION_NEXT_SYNC); - if (bitmap == null) { - bitmap = retriever.getFrameAtTime(time, MediaMetadataRetriever.OPTION_CLOSEST); + if (precise) { + AnimatedFileDrawable fileDrawable = new AnimatedFileDrawable(new File(filePath), true, 0, null, null, null, 0, 0, true); + bitmap = fileDrawable.getFrameAtTime(time, precise); + if (orientation != null) { + orientation[0] = fileDrawable.getOrientation(); } - } catch (Exception ignore) { - // Assume this is a corrupt video file. - } finally { + fileDrawable.recycle(); + } else { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { - retriever.release(); - } catch (RuntimeException ex) { - // Ignore failures while cleaning up. + retriever.setDataSource(filePath); + bitmap = retriever.getFrameAtTime(time, MediaMetadataRetriever.OPTION_NEXT_SYNC); + if (bitmap == null) { + bitmap = retriever.getFrameAtTime(time, MediaMetadataRetriever.OPTION_CLOSEST); + } + } catch (Exception ignore) { + // Assume this is a corrupt video file. + } finally { + try { + retriever.release(); + } catch (RuntimeException ex) { + // Ignore failures while cleaning up. + } } } return bitmap; @@ -6594,37 +6633,35 @@ public class SendMessagesHelper extends BaseController implements NotificationCe videoEditedInfo.rotationValue = params[AnimatedFileDrawable.PARAM_NUM_ROTATION]; videoEditedInfo.originalDuration = (long) (videoDuration * 1000); - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - int selectedCompression = preferences.getInt("compress_video2", 1); int compressionsCount; - if (videoEditedInfo.originalWidth > 1280 || videoEditedInfo.originalHeight > 1280) { - compressionsCount = 5; - } else if (videoEditedInfo.originalWidth > 848 || videoEditedInfo.originalHeight > 848) { + + float maxSize = Math.max(videoEditedInfo.originalWidth, videoEditedInfo.originalHeight); + if (maxSize > 1280) { compressionsCount = 4; - } else if (videoEditedInfo.originalWidth > 640 || videoEditedInfo.originalHeight > 640) { + } else if (maxSize > 854) { compressionsCount = 3; - } else if (videoEditedInfo.originalWidth > 480 || videoEditedInfo.originalHeight > 480) { + } else if (maxSize > 640) { compressionsCount = 2; } else { compressionsCount = 1; } + int selectedCompression = Math.round(DownloadController.getInstance(UserConfig.selectedAccount).getMaxVideoBitrate() / (100f / compressionsCount)) - 1; + if (selectedCompression >= compressionsCount) { selectedCompression = compressionsCount - 1; } if (selectedCompression != compressionsCount - 1) { - float maxSize; switch (selectedCompression) { - case 0: + case 1: maxSize = 432.0f; break; - case 1: + case 2: maxSize = 640.0f; break; - case 2: + case 3: maxSize = 848.0f; break; - case 3: default: maxSize = 1280.0f; break; @@ -6659,12 +6696,11 @@ public class SendMessagesHelper extends BaseController implements NotificationCe } @UiThread - public static void prepareSendingVideo(AccountInstance accountInstance, final String videoPath, final long estimatedSize, final long duration, final int width, final int height, final VideoEditedInfo info, final long dialog_id, final MessageObject reply_to_msg, final CharSequence caption, final ArrayList entities, final int ttl, final MessageObject editingMessageObject, boolean notify, int scheduleDate) { + public static void prepareSendingVideo(AccountInstance accountInstance, final String videoPath, final VideoEditedInfo info, final long dialog_id, final MessageObject reply_to_msg, final CharSequence caption, final ArrayList entities, final int ttl, final MessageObject editingMessageObject, boolean notify, int scheduleDate) { if (videoPath == null || videoPath.length() == 0) { return; } new Thread(() -> { - final VideoEditedInfo videoEditedInfo = info != null ? info : createCompressionSettings(videoPath); boolean isEncrypted = (int) dialog_id == 0; @@ -6682,7 +6718,7 @@ public class SendMessagesHelper extends BaseController implements NotificationCe originalPath += temp.length() + "_" + temp.lastModified(); if (videoEditedInfo != null) { if (!isRound) { - originalPath += duration + "_" + videoEditedInfo.startTime + "_" + videoEditedInfo.endTime + (videoEditedInfo.muted ? "_m" : ""); + originalPath += videoEditedInfo.estimatedDuration + "_" + videoEditedInfo.startTime + "_" + videoEditedInfo.endTime + (videoEditedInfo.muted ? "_m" : ""); if (videoEditedInfo.resultWidth != videoEditedInfo.originalWidth) { originalPath += "_" + videoEditedInfo.resultWidth; } @@ -6691,9 +6727,9 @@ public class SendMessagesHelper extends BaseController implements NotificationCe } TLRPC.TL_document document = null; String parentObject = null; - if (!isEncrypted && ttl == 0) { + if (!isEncrypted && ttl == 0 && (videoEditedInfo == null || videoEditedInfo.filterState == null && videoEditedInfo.paintPath == null && videoEditedInfo.mediaEntities == null && videoEditedInfo.cropState == null)) { Object[] sentData = accountInstance.getMessagesStorage().getSentFile(originalPath, !isEncrypted ? 2 : 5); - if (sentData != null) { + if (sentData != null && sentData[0] instanceof TLRPC.TL_document) { document = (TLRPC.TL_document) sentData[0]; parentObject = (String) sentData[1]; ensureMediaThumbExists(isEncrypted, document, videoPath, null, startTime); @@ -6755,16 +6791,27 @@ public class SendMessagesHelper extends BaseController implements NotificationCe videoEditedInfo.originalWidth = attributeVideo.w; videoEditedInfo.originalHeight = attributeVideo.h; } else { - attributeVideo.duration = (int) (duration / 1000); + attributeVideo.duration = (int) (videoEditedInfo.estimatedDuration / 1000); } - if (videoEditedInfo.rotationValue == 90 || videoEditedInfo.rotationValue == 270) { - attributeVideo.w = height; - attributeVideo.h = width; + + int w, h; + int rotation = videoEditedInfo.rotationValue; + if (videoEditedInfo.cropState != null) { + w = videoEditedInfo.cropState.transformWidth; + h = videoEditedInfo.cropState.transformHeight; + rotation += videoEditedInfo.cropState.transformRotation; } else { - attributeVideo.w = width; - attributeVideo.h = height; + w = videoEditedInfo.resultWidth; + h = videoEditedInfo.resultHeight; } - document.size = (int) estimatedSize; + if (rotation == 90 || rotation == 270) { + attributeVideo.w = h; + attributeVideo.h = w; + } else { + attributeVideo.w = w; + attributeVideo.h = h; + } + document.size = (int) videoEditedInfo.estimatedSize; } else { if (temp.exists()) { document.size = (int) temp.length(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java b/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java index 836b77e18..b4555c698 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SharedConfig.java @@ -56,6 +56,8 @@ import tw.nekomimi.nekogram.utils.UIUtil; import static com.v2ray.ang.V2RayConfig.SSR_PROTOCOL; import static com.v2ray.ang.V2RayConfig.SS_PROTOCOL; +import androidx.core.content.pm.ShortcutManagerCompat; + public class SharedConfig { public static String pushString = ""; @@ -63,7 +65,7 @@ public class SharedConfig { public static byte[] pushAuthKey; public static byte[] pushAuthKeyId; - public static long directShareHash; + public static String directShareHash; public static boolean saveIncomingPhotos; public static String passcodeHash = ""; @@ -101,6 +103,7 @@ public class SharedConfig { public static boolean saveToGallery; public static int mapPreviewType = 2; + public static boolean chatBubbles = false; public static boolean autoplayGifs = true; public static boolean autoplayVideo = true; public static boolean raiseToSpeak = true; @@ -1081,13 +1084,14 @@ public class SharedConfig { preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); saveToGallery = preferences.getBoolean("save_gallery", false); autoplayGifs = preferences.getBoolean("autoplay_gif", true); + chatBubbles = preferences.getBoolean("chatBubbles", false); autoplayVideo = preferences.getBoolean("autoplay_video", true); mapPreviewType = preferences.getInt("mapPreviewType", 2); raiseToSpeak = preferences.getBoolean("raise_to_speak", true); customTabs = preferences.getBoolean("custom_tabs", true); directShare = preferences.getBoolean("direct_share", true); shuffleMusic = preferences.getBoolean("shuffleMusic", false); - playOrderReversed = preferences.getBoolean("playOrderReversed", false); + playOrderReversed = !shuffleMusic && preferences.getBoolean("playOrderReversed", false); inappCamera = preferences.getBoolean("inappCamera", true); hasCameraCache = preferences.contains("cameraCache"); roundCamera16to9 = true;//preferences.getBoolean("roundCamera16to9", false); @@ -1107,7 +1111,7 @@ public class SharedConfig { sortContactsByName = preferences.getBoolean("sortContactsByName", false); sortFilesByName = preferences.getBoolean("sortFilesByName", false); noSoundHintShowed = preferences.getBoolean("noSoundHintShowed", false); - directShareHash = preferences.getLong("directShareHash", 0); + directShareHash = preferences.getString("directShareHash2", null); useThreeLinesLayout = preferences.getBoolean("useThreeLinesLayout", false); archiveHidden = preferences.getBoolean("archiveHidden", false); distanceSystemType = preferences.getInt("distanceSystemType", 0); @@ -1384,11 +1388,16 @@ public class SharedConfig { editor.commit(); } - public static void toggleShuffleMusic(int type) { + public static void setPlaybackOrderType(int type) { if (type == 2) { - shuffleMusic = !shuffleMusic; + shuffleMusic = true; + playOrderReversed = false; + } else if (type == 1) { + playOrderReversed = true; + shuffleMusic = false; } else { - playOrderReversed = !playOrderReversed; + playOrderReversed = false; + shuffleMusic = false; } MediaController.getInstance().checkIsNextMediaFileDownloaded(); SharedPreferences preferences = MessagesController.getGlobalMainSettings(); @@ -1398,9 +1407,9 @@ public class SharedConfig { editor.commit(); } - public static void toggleRepeatMode() { - repeatMode++; - if (repeatMode > 2) { + public static void setRepeatMode(int mode) { + repeatMode = mode; + if (repeatMode < 0 || repeatMode > 2) { repeatMode = 0; } SharedPreferences preferences = MessagesController.getGlobalMainSettings(); @@ -1426,6 +1435,14 @@ public class SharedConfig { editor.commit(); } + public static void toggleChatBubbles() { + chatBubbles = !chatBubbles; + SharedPreferences preferences = MessagesController.getGlobalMainSettings(); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean("chatBubbles", chatBubbles); + editor.commit(); + } + public static void setUseThreeLinesLayout(boolean value) { useThreeLinesLayout = value; SharedPreferences preferences = MessagesController.getGlobalMainSettings(); @@ -1497,6 +1514,8 @@ public class SharedConfig { SharedPreferences.Editor editor = preferences.edit(); editor.putBoolean("direct_share", directShare); editor.commit(); + ShortcutManagerCompat.removeAllDynamicShortcuts(ApplicationLoader.applicationContext); + MediaDataController.getInstance(UserConfig.selectedAccount).buildShortcuts(); } public static void toggleStreamMedia() { @@ -2035,7 +2054,7 @@ public class SharedConfig { public final static int PERFORMANCE_CLASS_AVERAGE = 1; public final static int PERFORMANCE_CLASS_HIGH = 2; - public static int getDevicePerfomanceClass() { + public static int getDevicePerformanceClass() { if (devicePerformanceClass == -1) { int maxCpuFreq = -1; try { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/TgChooserTargetService.java b/TMessagesProj/src/main/java/org/telegram/messenger/TgChooserTargetService.java deleted file mode 100644 index ad14defe8..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/TgChooserTargetService.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 5.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-2018. - */ - -package org.telegram.messenger; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.ComponentName; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.RectF; -import android.graphics.Shader; -import android.graphics.drawable.Icon; -import android.os.Build; -import android.os.Bundle; -import android.service.chooser.ChooserTarget; -import android.service.chooser.ChooserTargetService; -import android.text.TextUtils; - -import org.telegram.SQLite.SQLiteCursor; -import org.telegram.tgnet.TLRPC; -import org.telegram.ui.Components.AvatarDrawable; -import org.telegram.ui.LaunchActivity; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CountDownLatch; - -@TargetApi(Build.VERSION_CODES.M) -public class TgChooserTargetService extends ChooserTargetService { - - private Paint roundPaint; - private RectF bitmapRect; - - @Override - public List onGetChooserTargets(ComponentName targetActivityName, IntentFilter matchedFilter) { - final int currentAccount = UserConfig.selectedAccount; - - final List targets = new ArrayList<>(); - if (!UserConfig.getInstance(currentAccount).isClientActivated()) { - return targets; - } - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - if (!preferences.getBoolean("direct_share", true)) { - return targets; - } - if (AndroidUtilities.needShowPasscode() || SharedConfig.isWaitingForPasscodeEnter) { - return targets; - } - - ImageLoader imageLoader = ImageLoader.getInstance(); - final CountDownLatch countDownLatch = new CountDownLatch(1); - final ComponentName componentName = new ComponentName(getPackageName(), LaunchActivity.class.getCanonicalName()); - MessagesStorage.getInstance(currentAccount).getStorageQueue().postRunnable(() -> { - ArrayList dialogs = new ArrayList<>(); - ArrayList chats = new ArrayList<>(); - ArrayList users = new ArrayList<>(); - try { - ArrayList usersToLoad = new ArrayList<>(); - usersToLoad.add(UserConfig.getInstance(currentAccount).getClientUserId()); - ArrayList chatsToLoad = new ArrayList<>(); - SQLiteCursor cursor = MessagesStorage.getInstance(currentAccount).getDatabase().queryFinalized(String.format(Locale.US, "SELECT did FROM dialogs ORDER BY date DESC LIMIT %d,%d", 0, 30)); - while (cursor.next()) { - long id = cursor.longValue(0); - - int lower_id = (int) id; - int high_id = (int) (id >> 32); - if (lower_id != 0) { - if (lower_id > 0) { - if (!usersToLoad.contains(lower_id)) { - usersToLoad.add(lower_id); - } - } else { - if (!chatsToLoad.contains(-lower_id)) { - chatsToLoad.add(-lower_id); - } - } - } else { - continue; - } - dialogs.add(lower_id); - if (dialogs.size() == 8) { - break; - } - } - cursor.dispose(); - if (!chatsToLoad.isEmpty()) { - MessagesStorage.getInstance(currentAccount).getChatsInternal(TextUtils.join(",", chatsToLoad), chats); - } - if (!usersToLoad.isEmpty()) { - MessagesStorage.getInstance(currentAccount).getUsersInternal(TextUtils.join(",", usersToLoad), users); - } - } catch (Exception e) { - FileLog.e(e); - } - SharedConfig.directShareHash = Utilities.random.nextLong(); - ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).edit().putLong("directShareHash", SharedConfig.directShareHash).commit(); - - for (int a = 0; a < dialogs.size(); a++) { - Bundle extras = new Bundle(); - Icon icon = null; - String name = null; - int id = dialogs.get(a); - if (id > 0) { - for (int b = 0; b < users.size(); b++) { - TLRPC.User user = users.get(b); - if (user.id == id) { - if (!user.bot) { - extras.putLong("dialogId", (long) id); - extras.putLong("hash", SharedConfig.directShareHash); - if (UserObject.isUserSelf(user)) { - name = LocaleController.getString("SavedMessages", R.string.SavedMessages); - icon = createSavedMessagesIcon(); - } else { - name = ContactsController.formatName(user.first_name, user.last_name); - if (user.photo != null && user.photo.photo_small != null) { - icon = createRoundBitmap(FileLoader.getPathToAttach(user.photo.photo_small, true)); - } - } - } - break; - } - } - } else { - for (int b = 0; b < chats.size(); b++) { - TLRPC.Chat chat = chats.get(b); - if (chat.id == -id) { - if (!ChatObject.isNotInChat(chat) && (!ChatObject.isChannel(chat) || chat.megagroup)) { - extras.putLong("dialogId", (long) id); - extras.putLong("hash", SharedConfig.directShareHash); - if (chat.photo != null && chat.photo.photo_small != null) { - icon = createRoundBitmap(FileLoader.getPathToAttach(chat.photo.photo_small, true)); - } - name = chat.title; - } - break; - } - } - } - if (name != null) { - if (icon == null) { - icon = Icon.createWithResource(ApplicationLoader.applicationContext, R.drawable.logo_avatar); - } - targets.add(new ChooserTarget(name, icon, 1.0f, componentName, extras)); - } - } - countDownLatch.countDown(); - }); - try { - countDownLatch.await(); - } catch (Exception e) { - FileLog.e(e); - } - return targets; - } - - private Icon createRoundBitmap(File path) { - try { - Bitmap bitmap = BitmapFactory.decodeFile(path.toString()); - if (bitmap != null) { - Bitmap result = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); - result.eraseColor(Color.TRANSPARENT); - Canvas canvas = new Canvas(result); - BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - if (roundPaint == null) { - roundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - bitmapRect = new RectF(); - } - roundPaint.setShader(shader); - bitmapRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); - canvas.drawRoundRect(bitmapRect, bitmap.getWidth(), bitmap.getHeight(), roundPaint); - return Icon.createWithBitmap(result); - } - } catch (Throwable e) { - FileLog.e(e); - } - return null; - } - - private Icon createSavedMessagesIcon() { - try { - final Bitmap bitmap = Bitmap.createBitmap(AndroidUtilities.dp(56f), AndroidUtilities.dp(56f), Bitmap.Config.ARGB_8888); - final AvatarDrawable avatarDrawable = new AvatarDrawable(); - avatarDrawable.setAvatarType(AvatarDrawable.AVATAR_TYPE_SAVED); - avatarDrawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); - avatarDrawable.draw(new Canvas(bitmap)); - return Icon.createWithBitmap(bitmap); - } catch (Throwable e) { - FileLog.e(e); - } - return null; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java index ea003907d..baf650911 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java @@ -23,11 +23,13 @@ public class VideoEditedInfo { public long startTime; public long endTime; + public long avatarStartTime = -1; public float start; public float end; public int rotationValue; public int originalWidth; public int originalHeight; + public int originalBitrate; public int resultWidth; public int resultHeight; public int bitrate; @@ -45,6 +47,7 @@ public class VideoEditedInfo { public MediaController.SavedFilterState filterState; public String paintPath; public ArrayList mediaEntities; + public MediaController.CropState cropState; public boolean isPhoto; public boolean canceled; @@ -115,12 +118,34 @@ public class VideoEditedInfo { data.writeInt32(viewWidth); data.writeInt32(viewHeight); } + + public MediaEntity copy() { + MediaEntity entity = new MediaEntity(); + entity.type = type; + entity.subType = subType; + entity.x = x; + entity.y = y; + entity.rotation = rotation; + entity.width = width; + entity.height = height; + entity.text = text; + entity.color = color; + entity.fontSize = fontSize; + entity.viewWidth = viewWidth; + entity.viewHeight = viewHeight; + entity.scale = scale; + entity.textViewWidth = textViewWidth; + entity.textViewHeight = textViewHeight; + entity.textViewX = textViewX; + entity.textViewY = textViewY; + return entity; + } } public String getString() { String filters; - if (filterState != null || paintPath != null || mediaEntities != null && !mediaEntities.isEmpty()) { - int len = 2; + if (avatarStartTime != -1 || filterState != null || paintPath != null || mediaEntities != null && !mediaEntities.isEmpty() || cropState != null) { + int len = 10; if (filterState != null) { len += 160; } @@ -132,10 +157,13 @@ public class VideoEditedInfo { paintPathBytes = null; } SerializedData serializedData = new SerializedData(len); - serializedData.writeInt32(1); + serializedData.writeInt32(5); + serializedData.writeInt64(avatarStartTime); + serializedData.writeInt32(originalBitrate); if (filterState != null) { serializedData.writeByte(1); serializedData.writeFloat(filterState.enhanceValue); + serializedData.writeFloat(filterState.softenSkinValue); serializedData.writeFloat(filterState.exposureValue); serializedData.writeFloat(filterState.contrastValue); serializedData.writeFloat(filterState.warmthValue); @@ -196,6 +224,21 @@ public class VideoEditedInfo { } else { serializedData.writeByte(0); } + if (cropState != null) { + serializedData.writeByte(1); + serializedData.writeFloat(cropState.cropPx); + serializedData.writeFloat(cropState.cropPy); + serializedData.writeFloat(cropState.cropPw); + serializedData.writeFloat(cropState.cropPh); + serializedData.writeFloat(cropState.cropScale); + serializedData.writeFloat(cropState.cropRotate); + serializedData.writeInt32(cropState.transformWidth); + serializedData.writeInt32(cropState.transformHeight); + serializedData.writeInt32(cropState.transformRotation); + serializedData.writeBool(cropState.mirrored); + } else { + serializedData.writeByte(0); + } filters = Utilities.bytesToHex(serializedData.toByteArray()); serializedData.cleanup(); } else { @@ -229,10 +272,17 @@ public class VideoEditedInfo { if (s.length() > 0) { SerializedData serializedData = new SerializedData(Utilities.hexToBytes(s)); int version = serializedData.readInt32(false); + if (version >= 3) { + avatarStartTime = serializedData.readInt64(false); + originalBitrate = serializedData.readInt32(false); + } byte has = serializedData.readByte(false); if (has != 0) { filterState = new MediaController.SavedFilterState(); filterState.enhanceValue = serializedData.readFloat(false); + if (version >= 5) { + filterState.softenSkinValue = serializedData.readFloat(false); + } filterState.exposureValue = serializedData.readFloat(false); filterState.contrastValue = serializedData.readFloat(false); filterState.warmthValue = serializedData.readFloat(false); @@ -284,6 +334,24 @@ public class VideoEditedInfo { } isPhoto = serializedData.readByte(false) == 1; } + if (version >= 2) { + has = serializedData.readByte(false); + if (has != 0) { + cropState = new MediaController.CropState(); + cropState.cropPx = serializedData.readFloat(false); + cropState.cropPy = serializedData.readFloat(false); + cropState.cropPw = serializedData.readFloat(false); + cropState.cropPh = serializedData.readFloat(false); + cropState.cropScale = serializedData.readFloat(false); + cropState.cropRotate = serializedData.readFloat(false); + cropState.transformWidth = serializedData.readInt32(false); + cropState.transformHeight = serializedData.readInt32(false); + cropState.transformRotation = serializedData.readInt32(false); + if (version >= 4) { + cropState.mirrored = serializedData.readBool(false); + } + } + } serializedData.cleanup(); } } else { @@ -306,10 +374,10 @@ public class VideoEditedInfo { } public boolean needConvert() { - return !roundVideo || roundVideo && (startTime > 0 || endTime != -1 && endTime != estimatedDuration); + return mediaEntities != null || paintPath != null || filterState != null || cropState != null || !roundVideo || roundVideo && (startTime > 0 || endTime != -1 && endTime != estimatedDuration); } public boolean canAutoPlaySourceVideo() { - return roundVideo;// || (Math.max(originalHeight,originalWidth) <= 1920 && filterState == null) ; + return roundVideo; } } 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 0df4f2dfc..dbfd56a69 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java @@ -168,6 +168,13 @@ public class Browser { openUrl(context, Uri.parse(url), allowCustom, tryTelegraph); } + public static boolean isTelegraphUrl(String url, boolean equals) { + if (equals) { + return url.equals("telegra.ph") || url.equals("te.legra.ph") || url.equals("graph.org"); + } + return url.contains("telegra.ph") || url.contains("te.legra.ph") || url.contains("graph.org"); + } + public static void openUrl(final Context context, Uri uri, final boolean allowCustom, boolean tryTelegraph) { if (context == null || uri == null) { return; @@ -178,7 +185,7 @@ public class Browser { if (tryTelegraph) { try { String host = uri.getHost().toLowerCase(); - if (host.equals("telegra.ph") || uri.toString().toLowerCase().contains("telegram.org/faq")) { + if (isTelegraphUrl(host, true) || uri.toString().toLowerCase().contains("telegram.org/faq")) { final AlertDialog[] progressDialog = new AlertDialog[]{new AlertDialog(context, 3)}; Uri finalUri = uri; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraController.java b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraController.java index 70bb29590..0e3152318 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/camera/CameraController.java @@ -11,6 +11,7 @@ package org.telegram.messenger.camera; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.SurfaceTexture; import android.graphics.drawable.BitmapDrawable; @@ -57,6 +58,7 @@ public class CameraController implements MediaRecorder.OnInfoListener { protected ArrayList availableFlashModes = new ArrayList<>(); private MediaRecorder recorder; private String recordedFile; + private boolean mirrorRecorderVideo; protected volatile ArrayList cameraInfos; private VideoTakeCallback onVideoTakeCallback; private boolean cameraInitied; @@ -589,7 +591,7 @@ public class CameraController implements MediaRecorder.OnInfoListener { }); } - public void recordVideo(final CameraSession session, final File path, final VideoTakeCallback callback, final Runnable onVideoStartRecord) { + public void recordVideo(final CameraSession session, final File path, boolean mirror, final VideoTakeCallback callback, final Runnable onVideoStartRecord) { if (session == null) { return; } @@ -609,6 +611,7 @@ public class CameraController implements MediaRecorder.OnInfoListener { camera.unlock(); //camera.stopPreview(); try { + mirrorRecorderVideo = mirror; recorder = new MediaRecorder(); recorder.setCamera(camera); recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); @@ -671,7 +674,15 @@ public class CameraController implements MediaRecorder.OnInfoListener { FileLog.e(e); } } - final Bitmap bitmap = SendMessagesHelper.createVideoThumbnail(recordedFile, MediaStore.Video.Thumbnails.MINI_KIND); + Bitmap bitmap = SendMessagesHelper.createVideoThumbnail(recordedFile, MediaStore.Video.Thumbnails.MINI_KIND); + if (mirrorRecorderVideo) { + Bitmap b = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(b); + canvas.scale(-1, 1, b.getWidth() / 2, b.getHeight() / 2); + canvas.drawBitmap(bitmap, 0, 0, null); + bitmap.recycle(); + bitmap = b; + } String fileName = Integer.MIN_VALUE + "_" + SharedConfig.getLastLocalId() + ".jpg"; final File cacheFile = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName); try { @@ -682,11 +693,12 @@ public class CameraController implements MediaRecorder.OnInfoListener { } SharedConfig.saveConfig(); final long durationFinal = duration; + final Bitmap bitmapFinal = bitmap; AndroidUtilities.runOnUIThread(() -> { if (onVideoTakeCallback != null) { String path = cacheFile.getAbsolutePath(); - if (bitmap != null) { - ImageLoader.getInstance().putImageToCache(new BitmapDrawable(bitmap), Utilities.MD5(path)); + if (bitmapFinal != null) { + ImageLoader.getInstance().putImageToCache(new BitmapDrawable(bitmapFinal), Utilities.MD5(path)); } onVideoTakeCallback.onFinishVideoRecording(path, durationFinal); onVideoTakeCallback = null; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/secretmedia/ExtendedDefaultDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/secretmedia/ExtendedDefaultDataSource.java index 1e421d780..ab1798267 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/secretmedia/ExtendedDefaultDataSource.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/secretmedia/ExtendedDefaultDataSource.java @@ -169,8 +169,6 @@ public final class ExtendedDefaultDataSource implements DataSource { listener, new DefaultHttpDataSource( userAgent, - /* contentTypePredicate= */ null, - listener, connectTimeoutMillis, readTimeoutMillis, allowCrossProtocolRedirects, @@ -194,6 +192,7 @@ public final class ExtendedDefaultDataSource implements DataSource { this(context, baseDataSource); if (listener != null) { transferListeners.add(listener); + baseDataSource.addTransferListener(listener); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/video/MP4Builder.java b/TMessagesProj/src/main/java/org/telegram/messenger/video/MP4Builder.java index 3f891f9d1..b31595cda 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/MP4Builder.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/MP4Builder.java @@ -169,6 +169,10 @@ public class MP4Builder { return 0; } + public long getLastFrameTimestamp(int trackIndex) { + return currentMp4Movie.getLastFrameTimestamp(trackIndex); + } + public int addTrack(MediaFormat mediaFormat, boolean isAudio) { return currentMp4Movie.addTrack(mediaFormat, isAudio); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/video/MediaCodecVideoConvertor.java b/TMessagesProj/src/main/java/org/telegram/messenger/video/MediaCodecVideoConvertor.java index 8fd78e9ec..ddad25359 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/MediaCodecVideoConvertor.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/MediaCodecVideoConvertor.java @@ -10,7 +10,6 @@ import android.os.Build; import org.telegram.messenger.BuildVars; import org.telegram.messenger.FileLog; import org.telegram.messenger.MediaController; -import org.telegram.messenger.Utilities; import org.telegram.messenger.VideoEditedInfo; import java.io.File; @@ -19,10 +18,12 @@ import java.util.ArrayList; public class MediaCodecVideoConvertor { - MP4Builder mediaMuxer; - MediaExtractor extractor; + private MP4Builder mediaMuxer; + private MediaExtractor extractor; - MediaController.VideoConvertorListener callback; + private long endPresentationTime; + + private MediaController.VideoConvertorListener callback; private final static int PROCESSOR_TYPE_OTHER = 0; private final static int PROCESSOR_TYPE_QCOM = 1; @@ -37,40 +38,47 @@ public class MediaCodecVideoConvertor { public boolean convertVideo(String videoPath, File cacheFile, int rotationValue, boolean isSecret, int resultWidth, int resultHeight, - int framerate, int bitrate, - long startTime, long endTime, + int framerate, int bitrate, int originalBitrate, + long startTime, long endTime, long avatarStartTime, boolean needCompress, long duration, MediaController.SavedFilterState savedFilterState, String paintPath, ArrayList mediaEntities, boolean isPhoto, + MediaController.CropState cropState, MediaController.VideoConvertorListener callback) { this.callback = callback; return convertVideoInternal(videoPath, cacheFile, rotationValue, isSecret, - resultWidth, resultHeight, framerate, bitrate, startTime, endTime, duration, needCompress, false, savedFilterState, paintPath, mediaEntities, isPhoto); + resultWidth, resultHeight, framerate, bitrate, originalBitrate, startTime, endTime, avatarStartTime, duration, needCompress, false, savedFilterState, paintPath, mediaEntities, isPhoto, cropState); + } + + public long getLastFrameTimestamp() { + return endPresentationTime; } @TargetApi(18) private boolean convertVideoInternal(String videoPath, File cacheFile, int rotationValue, boolean isSecret, int resultWidth, int resultHeight, - int framerate, int bitrate, - long startTime, long endTime, + int framerate, int bitrate, int originalBitrate, + long startTime, long endTime, long avatarStartTime, long duration, boolean needCompress, boolean increaseTimeout, MediaController.SavedFilterState savedFilterState, String paintPath, ArrayList mediaEntities, - boolean isPhoto) { + boolean isPhoto, + MediaController.CropState cropState) { boolean error = false; boolean repeatWithIncreasedTimeout = false; + int videoTrackIndex = -5; try { MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); Mp4Movie movie = new Mp4Movie(); movie.setCacheFile(cacheFile); - movie.setRotation(rotationValue); + movie.setRotation(0); movie.setSize(resultWidth, resultHeight); mediaMuxer = new MP4Builder().createMovie(movie, isSecret); @@ -80,15 +88,24 @@ public class MediaCodecVideoConvertor { InputSurface inputSurface = null; OutputSurface outputSurface = null; int prependHeaderSize = 0; + endPresentationTime = duration * 1000; checkConversionCanceled(); if (isPhoto) { try { boolean outputDone = false; boolean decoderDone = false; - int videoTrackIndex = -5; int framesCount = 0; - if (bitrate <= 0) { + + if (avatarStartTime >= 0) { + if (durationS <= 2000) { + bitrate = 2600000; + } else if (durationS <= 5000) { + bitrate = 2200000; + } else { + bitrate = 1560000; + } + } else if (bitrate <= 0) { bitrate = 921600; } @@ -121,7 +138,7 @@ public class MediaCodecVideoConvertor { inputSurface.makeCurrent(); encoder.start(); - outputSurface = new OutputSurface(savedFilterState, videoPath, paintPath, mediaEntities, resultWidth, resultHeight, framerate, true); + outputSurface = new OutputSurface(savedFilterState, videoPath, paintPath, mediaEntities, null, resultWidth, resultHeight, rotationValue, framerate, true); ByteBuffer[] encoderOutputBuffers = null; ByteBuffer[] encoderInputBuffers = null; @@ -200,9 +217,6 @@ public class MediaCodecVideoConvertor { long availableSize = mediaMuxer.writeSampleData(videoTrackIndex, encodedData, info, true); if (availableSize != 0) { if (callback != null) { - if (info.presentationTimeUs - startTime > currentPts) { - currentPts = info.presentationTimeUs - startTime; - } callback.didWriteData(availableSize, (currentPts / 1000f) / durationS); } } @@ -243,8 +257,9 @@ public class MediaCodecVideoConvertor { } if (!decoderDone) { - outputSurface.drawImage(false); - inputSurface.setPresentationTime((long) (framesCount / 30.0f * 1000L * 1000L * 1000L)); + outputSurface.drawImage(); + long presentationTime = (long) (framesCount / 30.0f * 1000L * 1000L * 1000L); + inputSurface.setPresentationTime(presentationTime); inputSurface.swapBuffers(); framesCount++; @@ -304,120 +319,81 @@ public class MediaCodecVideoConvertor { boolean inputDone = false; boolean decoderDone = false; int swapUV = 0; - int videoTrackIndex = -5; int audioTrackIndex = -5; - - int colorFormat; - int processorType = PROCESSOR_TYPE_OTHER; - String manufacturer = Build.MANUFACTURER.toLowerCase(); - if (Build.VERSION.SDK_INT < 18) { - MediaCodecInfo codecInfo = MediaController.selectCodec(MediaController.VIDEO_MIME_TYPE); - colorFormat = MediaController.selectColorFormat(codecInfo, MediaController.VIDEO_MIME_TYPE); - if (colorFormat == 0) { - throw new RuntimeException("no supported color format"); - } - String codecName = codecInfo.getName(); - if (codecName.contains("OMX.qcom.")) { - processorType = PROCESSOR_TYPE_QCOM; - if (Build.VERSION.SDK_INT == 16) { - if (manufacturer.equals("lge") || manufacturer.equals("nokia")) { - swapUV = 1; - } - } - } else if (codecName.contains("OMX.Intel.")) { - processorType = PROCESSOR_TYPE_INTEL; - } else if (codecName.equals("OMX.MTK.VIDEO.ENCODER.AVC")) { - processorType = PROCESSOR_TYPE_MTK; - } else if (codecName.equals("OMX.SEC.AVC.Encoder")) { - processorType = PROCESSOR_TYPE_SEC; - swapUV = 1; - } else if (codecName.equals("OMX.TI.DUCATI1.VIDEO.H264E")) { - processorType = PROCESSOR_TYPE_TI; - } - if (BuildVars.LOGS_ENABLED) { - FileLog.d("codec = " + codecInfo.getName() + " manufacturer = " + manufacturer + "device = " + Build.MODEL); - } - } else { - colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; - } - if (BuildVars.LOGS_ENABLED) { - FileLog.d("colorFormat = " + colorFormat); - } - - int resultHeightAligned = resultHeight; - int padding = 0; - int bufferSize = resultWidth * resultHeight * 3 / 2; - if (processorType == PROCESSOR_TYPE_OTHER) { - if (resultHeight % 16 != 0) { - resultHeightAligned += (16 - (resultHeight % 16)); - padding = resultWidth * (resultHeightAligned - resultHeight); - bufferSize += padding * 5 / 4; - } - } else if (processorType == PROCESSOR_TYPE_QCOM) { - if (!manufacturer.toLowerCase().equals("lge")) { - int uvoffset = (resultWidth * resultHeight + 2047) & ~2047; - padding = uvoffset - (resultWidth * resultHeight); - bufferSize += padding; - } - } else if (processorType == PROCESSOR_TYPE_TI) { - //resultHeightAligned = 368; - //bufferSize = resultWidth * resultHeightAligned * 3 / 2; - //resultHeightAligned += (16 - (resultHeight % 16)); - //padding = resultWidth * (resultHeightAligned - resultHeight); - //bufferSize += padding * 5 / 4; - } else if (processorType == PROCESSOR_TYPE_MTK) { - if (manufacturer.equals("baidu")) { - resultHeightAligned += (16 - (resultHeight % 16)); - padding = resultWidth * (resultHeightAligned - resultHeight); - bufferSize += padding * 5 / 4; - } - } + long additionalPresentationTime = 0; + long minPresentationTime = Integer.MIN_VALUE; + long frameDelta = 1000 / framerate * 1000; extractor.selectTrack(videoIndex); MediaFormat videoFormat = extractor.getTrackFormat(videoIndex); - if (startTime > 0) { + if (avatarStartTime >= 0) { + if (durationS <= 2000) { + bitrate = 2600000; + } else if (durationS <= 5000) { + bitrate = 2200000; + } else { + bitrate = 1560000; + } + avatarStartTime = 0; + } else if (bitrate <= 0) { + bitrate = 921600; + } + if (originalBitrate > 0) { + bitrate = Math.min(originalBitrate, bitrate); + } + + long trueStartTime;// = startTime < 0 ? 0 : startTime; + if (avatarStartTime >= 0/* && trueStartTime == avatarStartTime*/) { + avatarStartTime = -1; + } + + if (avatarStartTime >= 0) { + extractor.seekTo(avatarStartTime, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + } else if (startTime > 0) { extractor.seekTo(startTime, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); } else { extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); } - if (bitrate <= 0) { - bitrate = 921600; + int w; + int h; + if (cropState != null) { + if (rotationValue == 90 || rotationValue == 270) { + w = cropState.transformHeight; + h = cropState.transformWidth; + } else { + w = cropState.transformWidth; + h = cropState.transformHeight; + } + } else { + w = resultWidth; + h = resultHeight; } - - MediaFormat outputFormat = MediaFormat.createVideoFormat(MediaController.VIDEO_MIME_TYPE, resultWidth, resultHeight); - outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat); + if (BuildVars.LOGS_ENABLED) { + FileLog.d("create encoder with w = " + w + " h = " + h); + } + MediaFormat outputFormat = MediaFormat.createVideoFormat(MediaController.VIDEO_MIME_TYPE, w, h); + outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate); outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); - if (Build.VERSION.SDK_INT < 23 && Math.min(resultHeight, resultWidth) <= 480) { + if (Build.VERSION.SDK_INT < 23 && Math.min(h, w) <= 480) { if (bitrate > 921600) { bitrate = 921600; } outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); } - if (Build.VERSION.SDK_INT < 18) { - outputFormat.setInteger("stride", resultWidth + 32); - outputFormat.setInteger("slice-height", resultHeight); - } - encoder = MediaCodec.createEncoderByType(MediaController.VIDEO_MIME_TYPE); encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - if (Build.VERSION.SDK_INT >= 18) { - inputSurface = new InputSurface(encoder.createInputSurface()); - inputSurface.makeCurrent(); - } + inputSurface = new InputSurface(encoder.createInputSurface()); + inputSurface.makeCurrent(); encoder.start(); decoder = MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME)); - if (Build.VERSION.SDK_INT >= 18) { - outputSurface = new OutputSurface(savedFilterState, null, paintPath, mediaEntities, resultWidth, resultHeight, framerate, false); - } else { - outputSurface = new OutputSurface(resultWidth, resultHeight, rotationValue); - } + outputSurface = new OutputSurface(savedFilterState, null, paintPath, mediaEntities, cropState, resultWidth, resultHeight, rotationValue, framerate, false); decoder.configure(videoFormat, outputSurface.getSurface(), null, 0); decoder.start(); @@ -427,15 +403,11 @@ public class MediaCodecVideoConvertor { if (Build.VERSION.SDK_INT < 21) { decoderInputBuffers = decoder.getInputBuffers(); encoderOutputBuffers = encoder.getOutputBuffers(); - if (Build.VERSION.SDK_INT < 18) { - encoderInputBuffers = encoder.getInputBuffers(); - } } if (audioIndex >= 0) { MediaFormat audioFormat = extractor.getTrackFormat(audioIndex); - copyAudioBuffer = audioFormat.getString(MediaFormat.KEY_MIME).equals(MediaController.AUIDO_MIME_TYPE) - || audioFormat.getString(MediaFormat.KEY_MIME).equals("audio/mpeg"); + copyAudioBuffer = audioFormat.getString(MediaFormat.KEY_MIME).equals(MediaController.AUIDO_MIME_TYPE) || audioFormat.getString(MediaFormat.KEY_MIME).equals("audio/mpeg"); if (audioFormat.getString(MediaFormat.KEY_MIME).equals("audio/unknown")) { audioIndex = -1; @@ -632,7 +604,7 @@ public class MediaCodecVideoConvertor { } } - MediaFormat newFormat = MediaFormat.createVideoFormat(MediaController.VIDEO_MIME_TYPE, resultWidth, resultHeight); + MediaFormat newFormat = MediaFormat.createVideoFormat(MediaController.VIDEO_MIME_TYPE, w, h); if (sps != null && pps != null) { newFormat.setByteBuffer("csd-0", sps); newFormat.setByteBuffer("csd-1", pps); @@ -661,30 +633,57 @@ public class MediaCodecVideoConvertor { } else if (decoderStatus < 0) { throw new RuntimeException("unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus); } else { - boolean doRender; - if (Build.VERSION.SDK_INT >= 18) { - doRender = info.size != 0; - } else { - doRender = info.size != 0 || info.presentationTimeUs != 0; - } - if (endTime > 0 && info.presentationTimeUs >= endTime) { + boolean doRender = info.size != 0; + long originalPresentationTime = info.presentationTimeUs; + if (endTime > 0 && originalPresentationTime >= endTime) { inputDone = true; decoderDone = true; doRender = false; info.flags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM; } - if (startTime > 0 && videoTime == -1) { - if (info.presentationTimeUs < startTime) { + boolean flushed = false; + if (avatarStartTime >= 0 && (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0 && Math.abs(avatarStartTime - startTime) > 1000000 / framerate) { + if (startTime > 0) { + extractor.seekTo(startTime, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + } else { + extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + } + additionalPresentationTime = minPresentationTime + frameDelta; + endTime = avatarStartTime; + avatarStartTime = -1; + inputDone = false; + decoderDone = false; + doRender = false; + info.flags &=~ MediaCodec.BUFFER_FLAG_END_OF_STREAM; + decoder.flush(); + flushed = true; + } + trueStartTime = avatarStartTime >= 0 ? avatarStartTime : startTime; + if (trueStartTime > 0 && videoTime == -1) { + if (originalPresentationTime < trueStartTime) { doRender = false; if (BuildVars.LOGS_ENABLED) { - FileLog.d("drop frame startTime = " + startTime + " present time = " + info.presentationTimeUs); + FileLog.d("drop frame startTime = " + trueStartTime + " present time = " + info.presentationTimeUs); } } else { videoTime = info.presentationTimeUs; + if (minPresentationTime != Integer.MIN_VALUE) { + additionalPresentationTime -= videoTime; + } } } - decoder.releaseOutputBuffer(decoderStatus, doRender); + if (flushed) { + videoTime = -1; + } else { + if (avatarStartTime == -1 && additionalPresentationTime != 0) { + info.presentationTimeUs += additionalPresentationTime; + } + decoder.releaseOutputBuffer(decoderStatus, doRender); + } if (doRender) { + if (avatarStartTime >= 0) { + minPresentationTime = Math.max(minPresentationTime, info.presentationTimeUs); + } boolean errorWait = false; try { outputSurface.awaitNewImage(); @@ -693,25 +692,9 @@ public class MediaCodecVideoConvertor { FileLog.e(e); } if (!errorWait) { - if (Build.VERSION.SDK_INT >= 18) { - outputSurface.drawImage(false); - inputSurface.setPresentationTime(info.presentationTimeUs * 1000); - inputSurface.swapBuffers(); - } else { - int inputBufIndex = encoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT); - if (inputBufIndex >= 0) { - outputSurface.drawImage(true); - ByteBuffer rgbBuf = outputSurface.getFrame(); - ByteBuffer yuvBuf = encoderInputBuffers[inputBufIndex]; - yuvBuf.clear(); - Utilities.convertVideoFrame(rgbBuf, yuvBuf, colorFormat, resultWidth, resultHeight, padding, swapUV); - encoder.queueInputBuffer(inputBufIndex, 0, bufferSize, info.presentationTimeUs, 0); - } else { - if (BuildVars.LOGS_ENABLED) { - FileLog.d("input buffer not available"); - } - } - } + outputSurface.drawImage(); + inputSurface.setPresentationTime(info.presentationTimeUs * 1000); + inputSurface.swapBuffers(); } } if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { @@ -719,14 +702,7 @@ public class MediaCodecVideoConvertor { if (BuildVars.LOGS_ENABLED) { FileLog.d("decoder stream end"); } - if (Build.VERSION.SDK_INT >= 18) { - encoder.signalEndOfInputStream(); - } else { - int inputBufIndex = encoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT); - if (inputBufIndex >= 0) { - encoder.queueInputBuffer(inputBufIndex, 0, 1, info.presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM); - } - } + encoder.signalEndOfInputStream(); } } } @@ -779,6 +755,7 @@ public class MediaCodecVideoConvertor { if (mediaMuxer != null) { try { mediaMuxer.finishMovie(); + endPresentationTime = mediaMuxer.getLastFrameTimestamp(videoTrackIndex); } catch (Exception e) { FileLog.e(e); } @@ -787,7 +764,9 @@ public class MediaCodecVideoConvertor { if (repeatWithIncreasedTimeout) { return convertVideoInternal(videoPath, cacheFile, rotationValue, isSecret, - resultWidth, resultHeight, framerate, bitrate, startTime, endTime, duration, needCompress, true, savedFilterState, paintPath, mediaEntities, isPhoto); + resultWidth, resultHeight, framerate, bitrate, originalBitrate, startTime, endTime, avatarStartTime, duration, + needCompress, true, savedFilterState, paintPath, mediaEntities, + isPhoto, cropState); } return error; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/video/Mp4Movie.java b/TMessagesProj/src/main/java/org/telegram/messenger/video/Mp4Movie.java index 41d71e605..1a4526aeb 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/Mp4Movie.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/Mp4Movie.java @@ -76,4 +76,12 @@ public class Mp4Movie { tracks.add(new Track(tracks.size(), mediaFormat, isAudio)); return tracks.size() - 1; } + + public long getLastFrameTimestamp(int trackIndex) { + if (trackIndex < 0 || trackIndex >= tracks.size()) { + return 0; + } + Track track = tracks.get(trackIndex); + return track.getLastFrameTimestamp(); + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/video/OutputSurface.java b/TMessagesProj/src/main/java/org/telegram/messenger/video/OutputSurface.java index 907617ad5..96e049124 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/OutputSurface.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/OutputSurface.java @@ -1,30 +1,19 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * This is the source code of Telegram for Android v. 6.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). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright Nikolai Kudashov, 2013-2020. */ package org.telegram.messenger.video; import android.graphics.SurfaceTexture; -import android.opengl.GLES20; import android.view.Surface; import org.telegram.messenger.MediaController; import org.telegram.messenger.VideoEditedInfo; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.util.ArrayList; import javax.microedition.khronos.egl.EGL10; @@ -46,31 +35,9 @@ public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { private final Object mFrameSyncObject = new Object(); private boolean mFrameAvailable; private TextureRenderer mTextureRender; - private int mWidth; - private int mHeight; - private int rotateRender = 0; - private ByteBuffer mPixelBuf; - public OutputSurface(int width, int height, int rotate) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException(); - } - mWidth = width; - mHeight = height; - rotateRender = rotate; - mPixelBuf = ByteBuffer.allocateDirect(mWidth * mHeight * 4); - mPixelBuf.order(ByteOrder.LITTLE_ENDIAN); - eglSetup(width, height); - makeCurrent(); - setup(null, null, null, null, 0, 0, 0, false); - } - - public OutputSurface(MediaController.SavedFilterState savedFilterState, String imagePath, String paintPath, ArrayList mediaEntities, int w, int h, float fps, boolean photo) { - setup(savedFilterState, imagePath, paintPath, mediaEntities, w, h, fps, photo); - } - - private void setup(MediaController.SavedFilterState savedFilterState, String imagePath, String paintPath, ArrayList mediaEntities, int w, int h, float fps, boolean photo) { - mTextureRender = new TextureRenderer(rotateRender, savedFilterState, imagePath, paintPath, mediaEntities, w, h, fps, photo); + public OutputSurface(MediaController.SavedFilterState savedFilterState, String imagePath, String paintPath, ArrayList mediaEntities, MediaController.CropState cropState, int w, int h, int rotation, float fps, boolean photo) { + mTextureRender = new TextureRenderer(savedFilterState, imagePath, paintPath, mediaEntities, cropState, w, h, rotation, fps, photo); mTextureRender.surfaceCreated(); mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId()); mSurfaceTexture.setOnFrameAvailableListener(this); @@ -175,12 +142,11 @@ public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { } mFrameAvailable = false; } - mTextureRender.checkGlError("before updateTexImage"); mSurfaceTexture.updateTexImage(); } - public void drawImage(boolean invert) { - mTextureRender.drawFrame(mSurfaceTexture, invert); + public void drawImage() { + mTextureRender.drawFrame(mSurfaceTexture); } @Override @@ -194,12 +160,6 @@ public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { } } - public ByteBuffer getFrame() { - mPixelBuf.rewind(); - GLES20.glReadPixels(0, 0, mWidth, mHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPixelBuf); - return mPixelBuf; - } - private void checkEglError(String msg) { if (mEGL.eglGetError() != EGL10.EGL_SUCCESS) { throw new RuntimeException("EGL error encountered (see log)"); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/video/TextureRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/video/TextureRenderer.java index 120585de0..513ffa95b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/TextureRenderer.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/TextureRenderer.java @@ -1,5 +1,5 @@ /* - * This is the source code of Telegram for Android v. 5.x.x. + * This is the source code of Telegram for Android v. 6.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). * @@ -38,6 +38,7 @@ import android.view.inputmethod.EditorInfo; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.Bitmaps; +import org.telegram.messenger.BuildVars; import org.telegram.messenger.FileLog; import org.telegram.messenger.MediaController; import org.telegram.messenger.Utilities; @@ -55,6 +56,7 @@ public class TextureRenderer { private FloatBuffer verticesBuffer; private FloatBuffer textureBuffer; + private FloatBuffer renderTextureBuffer; private FloatBuffer bitmapVerticesBuffer; float[] bitmapData = { @@ -68,8 +70,10 @@ public class TextureRenderer { private String paintPath; private String imagePath; private ArrayList mediaEntities; - private int videoWidth; - private int videoHeight; + private int originalWidth; + private int originalHeight; + private int transformedWidth; + private int transformedHeight; private static final String VERTEX_SHADER = "uniform mat4 uMVPMatrix;\n" + @@ -82,7 +86,7 @@ public class TextureRenderer { " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + "}\n"; - private static final String FRAGMENT_SHADER = + private static final String FRAGMENT_EXTERNAL_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision highp float;\n" + "varying vec2 vTextureCoord;\n" + @@ -91,22 +95,29 @@ public class TextureRenderer { " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + "}\n"; + private static final String FRAGMENT_SHADER = + "precision highp float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform sampler2D sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + + "}\n"; + private float[] mMVPMatrix = new float[16]; private float[] mSTMatrix = new float[16]; - private int mTextureID = -12345; - private int mProgram; - private int muMVPMatrixHandle; - private int muSTMatrixHandle; - private int maPositionHandle; - private int maTextureHandle; + private float[] mSTMatrixIdentity = new float[16]; + private int mTextureID; + private int[] mProgram; + private int[] muMVPMatrixHandle; + private int[] muSTMatrixHandle; + private int[] maPositionHandle; + private int[] maTextureHandle; private int simpleShaderProgram; private int simplePositionHandle; private int simpleInputTexCoordHandle; private int simpleSourceImageHandle; - private int rotationAngle; - private int[] paintTexture; private int[] stickerTexture; private Bitmap stickerBitmap; @@ -118,15 +129,11 @@ public class TextureRenderer { private boolean isPhoto; - public TextureRenderer(int rotation, MediaController.SavedFilterState savedFilterState, String image, String paint, ArrayList entities, int w, int h, float fps, boolean photo) { - rotationAngle = rotation; + private boolean firstFrame = true; + + public TextureRenderer(MediaController.SavedFilterState savedFilterState, String image, String paint, ArrayList entities, MediaController.CropState cropState, int w, int h, int rotation, float fps, boolean photo) { isPhoto = photo; - float[] verticesData = { - -1.0f, -1.0f, - 1.0f, -1.0f, - -1.0f, 1.0f, - 1.0f, 1.0f, - }; + float[] texData = { 0.f, 0.f, 1.f, 0.f, @@ -134,102 +141,243 @@ public class TextureRenderer { 1.f, 1.f, }; - verticesBuffer = ByteBuffer.allocateDirect(verticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - verticesBuffer.put(verticesData).position(0); + if (BuildVars.LOGS_ENABLED) { + FileLog.d("start textureRenderer w = " + w + " h = " + h + " r = " + rotation + " fps = " + fps); + if (cropState != null) { + FileLog.d("cropState px = " + cropState.cropPx + " py = " + cropState.cropPy + " cScale = " + cropState.cropScale + + " cropRotate = " + cropState.cropRotate + " pw = " + cropState.cropPw + " ph = " + cropState.cropPh + + " tw = " + cropState.transformWidth + " th = " + cropState.transformHeight + " tr = " + cropState.transformRotation + + " mirror = " + cropState.mirrored); + } + } textureBuffer = ByteBuffer.allocateDirect(texData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); textureBuffer.put(texData).position(0); - verticesBuffer = ByteBuffer.allocateDirect(verticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); - verticesBuffer.put(verticesData).position(0); - bitmapVerticesBuffer = ByteBuffer.allocateDirect(bitmapData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); bitmapVerticesBuffer.put(bitmapData).position(0); Matrix.setIdentityM(mSTMatrix, 0); + Matrix.setIdentityM(mSTMatrixIdentity, 0); if (savedFilterState != null) { filterShaders = new FilterShaders(true); filterShaders.setDelegate(FilterShaders.getFilterShadersDelegate(savedFilterState)); } - videoWidth = w; - videoHeight = h; + transformedWidth = originalWidth = w; + transformedHeight = originalHeight = h; imagePath = image; paintPath = paint; mediaEntities = entities; videoFps = fps == 0 ? 30 : fps; + + int count; + if (filterShaders != null) { + count = 2; + } else { + count = 1; + } + mProgram = new int[count]; + muMVPMatrixHandle = new int[count]; + muSTMatrixHandle = new int[count]; + maPositionHandle = new int[count]; + maTextureHandle = new int[count]; + + Matrix.setIdentityM(mMVPMatrix, 0); + int textureRotation = 0; + if (cropState != null) { + float[] verticesData = { + 0, 0, + w, 0, + 0, h, + w, h, + }; + textureRotation = cropState.transformRotation; + if (textureRotation == 90 || textureRotation == 270) { + int temp = originalWidth; + originalWidth = originalHeight; + originalHeight = temp; + } + + transformedWidth *= cropState.cropPw; + transformedHeight *= cropState.cropPh; + + float angle = (float) (-cropState.cropRotate * (Math.PI / 180.0f)); + for (int a = 0; a < 4; a++) { + float x1 = verticesData[a * 2] - w / 2; + float y1 = verticesData[a * 2 + 1] - h / 2; + float x2 = (float) (x1 * Math.cos(angle) - y1 * Math.sin(angle) + cropState.cropPx * w) * cropState.cropScale; + float y2 = (float) (x1 * Math.sin(angle) + y1 * Math.cos(angle) - cropState.cropPy * h) * cropState.cropScale; + verticesData[a * 2] = x2 / transformedWidth * 2; + verticesData[a * 2 + 1] = y2 / transformedHeight * 2; + } + verticesBuffer = ByteBuffer.allocateDirect(verticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + verticesBuffer.put(verticesData).position(0); + } else { + float[] verticesData = { + -1.0f, -1.0f, + 1.0f, -1.0f, + -1.0f, 1.0f, + 1.0f, 1.0f, + }; + verticesBuffer = ByteBuffer.allocateDirect(verticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + verticesBuffer.put(verticesData).position(0); + } + float[] textureData; + if (filterShaders != null) { + if (textureRotation == 90) { + textureData = new float[]{ + 1.f, 1.f, + 1.f, 0.f, + 0.f, 1.f, + 0.f, 0.f + }; + } else if (textureRotation == 180) { + textureData = new float[]{ + 1.f, 0.f, + 0.f, 0.f, + 1.f, 1.f, + 0.f, 1.f + }; + } else if (textureRotation == 270) { + textureData = new float[]{ + 0.f, 0.f, + 0.f, 1.f, + 1.f, 0.f, + 1.f, 1.f + }; + } else { + textureData = new float[]{ + 0.f, 1.f, + 1.f, 1.f, + 0.f, 0.f, + 1.f, 0.f + }; + } + } else { + if (textureRotation == 90) { + textureData = new float[]{ + 1.f, 0.f, + 1.f, 1.f, + 0.f, 0.f, + 0.f, 1.f + }; + } else if (textureRotation == 180) { + textureData = new float[]{ + 1.f, 1.f, + 0.f, 1.f, + 1.f, 0.f, + 0.f, 0.f + }; + } else if (textureRotation == 270) { + textureData = new float[]{ + 0.f, 1.f, + 0.f, 0.f, + 1.f, 1.f, + 1.f, 0.f + }; + } else { + textureData = new float[]{ + 0.f, 0.f, + 1.f, 0.f, + 0.f, 1.f, + 1.f, 1.f + }; + } + } + if (cropState != null && cropState.mirrored) { + for (int a = 0; a < 4; a++) { + if (textureData[a * 2] > 0.5f) { + textureData[a * 2] = 0.0f; + } else { + textureData[a * 2] = 1.0f; + } + } + } + renderTextureBuffer = ByteBuffer.allocateDirect(textureData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + renderTextureBuffer.put(textureData).position(0); } public int getTextureId() { return mTextureID; } - public void drawFrame(SurfaceTexture st, boolean invert) { - checkGlError("onDrawFrame start"); + public void drawFrame(SurfaceTexture st) { if (isPhoto) { GLES20.glUseProgram(simpleShaderProgram); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glUniform1i(simpleSourceImageHandle, 0); GLES20.glEnableVertexAttribArray(simpleInputTexCoordHandle); - GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glVertexAttribPointer(simpleInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); GLES20.glEnableVertexAttribArray(simplePositionHandle); } else { st.getTransformMatrix(mSTMatrix); + if (BuildVars.LOGS_ENABLED && firstFrame) { + StringBuilder builder = new StringBuilder(); + for (int a = 0; a < mSTMatrix.length; a++) { + builder.append(mSTMatrix[a]).append(", "); + } + FileLog.d("stMatrix = " + builder); + firstFrame = false; + } if (blendEnabled) { GLES20.glDisable(GLES20.GL_BLEND); blendEnabled = false; } + + int texture; + int target; + int index; + float[] stMatrix; if (filterShaders != null) { filterShaders.onVideoFrameUpdate(mSTMatrix); - GLES20.glViewport(0, 0, videoWidth, videoHeight); + GLES20.glViewport(0, 0, originalWidth, originalHeight); + filterShaders.drawSkinSmoothPass(); filterShaders.drawEnhancePass(); filterShaders.drawSharpenPass(); filterShaders.drawCustomParamsPass(); boolean blurred = filterShaders.drawBlurPass(); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + if (transformedWidth != originalWidth || transformedHeight != originalHeight) { + GLES20.glViewport(0, 0, transformedWidth, transformedHeight); + } - GLES20.glUseProgram(simpleShaderProgram); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, filterShaders.getRenderTexture(blurred ? 0 : 1)); - - GLES20.glUniform1i(simpleSourceImageHandle, 0); - GLES20.glEnableVertexAttribArray(simpleInputTexCoordHandle); - GLES20.glVertexAttribPointer(simpleInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, filterShaders.getTextureBuffer()); - GLES20.glEnableVertexAttribArray(simplePositionHandle); - GLES20.glVertexAttribPointer(simplePositionHandle, 2, GLES20.GL_FLOAT, false, 8, filterShaders.getVertexBuffer()); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + texture = filterShaders.getRenderTexture(blurred ? 0 : 1); + index = 1; + target = GLES20.GL_TEXTURE_2D; + stMatrix = mSTMatrixIdentity; } else { - if (invert) { - mSTMatrix[5] = -mSTMatrix[5]; - mSTMatrix[13] = 1.0f - mSTMatrix[13]; - } - GLES20.glUseProgram(mProgram); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); - - GLES20.glVertexAttribPointer(maPositionHandle, 2, GLES20.GL_FLOAT, false, 8, verticesBuffer); - GLES20.glEnableVertexAttribArray(maPositionHandle); - GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); - GLES20.glEnableVertexAttribArray(maTextureHandle); - - GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0); - GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); - - if (paintTexture != null || stickerTexture != null) { - GLES20.glUseProgram(simpleShaderProgram); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - - GLES20.glUniform1i(simpleSourceImageHandle, 0); - GLES20.glEnableVertexAttribArray(simpleInputTexCoordHandle); - GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); - GLES20.glEnableVertexAttribArray(simplePositionHandle); - } + texture = mTextureID; + index = 0; + target = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + stMatrix = mSTMatrix; } + + GLES20.glUseProgram(mProgram[index]); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(target, texture); + + GLES20.glVertexAttribPointer(maPositionHandle[index], 2, GLES20.GL_FLOAT, false, 8, verticesBuffer); + GLES20.glEnableVertexAttribArray(maPositionHandle[index]); + GLES20.glVertexAttribPointer(maTextureHandle[index], 2, GLES20.GL_FLOAT, false, 8, renderTextureBuffer); + GLES20.glEnableVertexAttribArray(maTextureHandle[index]); + + GLES20.glUniformMatrix4fv(muSTMatrixHandle[index], 1, false, stMatrix, 0); + GLES20.glUniformMatrix4fv(muMVPMatrixHandle[index], 1, false, mMVPMatrix, 0); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + if (paintTexture != null || stickerTexture != null) { + GLES20.glUseProgram(simpleShaderProgram); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + + GLES20.glUniform1i(simpleSourceImageHandle, 0); + GLES20.glEnableVertexAttribArray(simpleInputTexCoordHandle); + GLES20.glVertexAttribPointer(simpleInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(simplePositionHandle); } if (paintTexture != null) { for (int a = 0; a < paintTexture.length; a++) { @@ -309,7 +457,7 @@ public class TextureRenderer { bitmapData[4] = temp; } if (rotation != 0) { - float ratio = videoWidth / (float) videoHeight; + float ratio = transformedWidth / (float) transformedHeight; float my = (bitmapData[5] + bitmapData[1]) / 2; for (int a = 0; a < 4; a++) { float x1 = bitmapData[a * 2 ] - mx; @@ -319,7 +467,7 @@ public class TextureRenderer { } } bitmapVerticesBuffer.put(bitmapData).position(0); - GLES20.glVertexAttribPointer(maPositionHandle, 2, GLES20.GL_FLOAT, false, 8, bitmapVerticesBuffer); + GLES20.glVertexAttribPointer(simplePositionHandle, 2, GLES20.GL_FLOAT, false, 8, bitmapVerticesBuffer); if (bind) { GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture); @@ -334,45 +482,21 @@ public class TextureRenderer { @SuppressLint("WrongConstant") public void surfaceCreated() { - mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); - if (mProgram == 0) { - throw new RuntimeException("failed creating program"); - } - maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); - checkGlError("glGetAttribLocation aPosition"); - if (maPositionHandle == -1) { - throw new RuntimeException("Could not get attrib location for aPosition"); - } - maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); - checkGlError("glGetAttribLocation aTextureCoord"); - if (maTextureHandle == -1) { - throw new RuntimeException("Could not get attrib location for aTextureCoord"); - } - muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); - checkGlError("glGetUniformLocation uMVPMatrix"); - if (muMVPMatrixHandle == -1) { - throw new RuntimeException("Could not get attrib location for uMVPMatrix"); - } - muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); - checkGlError("glGetUniformLocation uSTMatrix"); - if (muSTMatrixHandle == -1) { - throw new RuntimeException("Could not get attrib location for uSTMatrix"); + for (int a = 0; a < mProgram.length; a++) { + mProgram[a] = createProgram(VERTEX_SHADER, a == 0 ? FRAGMENT_EXTERNAL_SHADER : FRAGMENT_SHADER); + maPositionHandle[a] = GLES20.glGetAttribLocation(mProgram[a], "aPosition"); + maTextureHandle[a] = GLES20.glGetAttribLocation(mProgram[a], "aTextureCoord"); + muMVPMatrixHandle[a] = GLES20.glGetUniformLocation(mProgram[a], "uMVPMatrix"); + muSTMatrixHandle[a] = GLES20.glGetUniformLocation(mProgram[a], "uSTMatrix"); } int[] textures = new int[1]; GLES20.glGenTextures(1, textures, 0); mTextureID = textures[0]; GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); - checkGlError("glBindTexture mTextureID"); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); - checkGlError("glTexParameter"); - - Matrix.setIdentityM(mMVPMatrix, 0); - if (rotationAngle != 0) { - Matrix.rotateM(mMVPMatrix, 0, rotationAngle, 0, 0, 1); - } if (filterShaders != null || imagePath != null || paintPath != null || mediaEntities != null) { int vertexShader = FilterShaders.loadShader(GLES20.GL_VERTEX_SHADER, FilterShaders.simpleVertexShaderCode); @@ -400,7 +524,7 @@ public class TextureRenderer { if (filterShaders != null) { filterShaders.create(); - filterShaders.setRenderData(null, 0, mTextureID, videoWidth, videoHeight); + filterShaders.setRenderData(null, 0, mTextureID, originalWidth, originalHeight); } if (imagePath != null || paintPath != null) { paintTexture = new int[(imagePath != null ? 1 : 0) + (paintPath != null ? 1 : 0)]; @@ -434,14 +558,14 @@ public class TextureRenderer { Bitmap bitmap = BitmapFactory.decodeFile(path); if (bitmap != null) { if (a == 0 && imagePath != null) { - Bitmap newBitmap = Bitmap.createBitmap(videoWidth, videoHeight, Bitmap.Config.ARGB_8888); + Bitmap newBitmap = Bitmap.createBitmap(transformedWidth, transformedHeight, Bitmap.Config.ARGB_8888); newBitmap.eraseColor(0xff000000); Canvas canvas = new Canvas(newBitmap); float scale; if (angle == 90 || angle == 270) { - scale = Math.max(bitmap.getHeight() / (float) videoWidth, bitmap.getWidth() / (float) videoHeight); + scale = Math.max(bitmap.getHeight() / (float) transformedWidth, bitmap.getWidth() / (float) transformedHeight); } else { - scale = Math.max(bitmap.getWidth() / (float) videoWidth, bitmap.getHeight() / (float) videoHeight); + scale = Math.max(bitmap.getWidth() / (float) transformedWidth, bitmap.getHeight() / (float) transformedHeight); } android.graphics.Matrix matrix = new android.graphics.Matrix(); @@ -565,14 +689,11 @@ public class TextureRenderer { return 0; } int program = GLES20.glCreateProgram(); - checkGlError("glCreateProgram"); if (program == 0) { return 0; } GLES20.glAttachShader(program, vertexShader); - checkGlError("glAttachShader"); GLES20.glAttachShader(program, pixelShader); - checkGlError("glAttachShader"); GLES20.glLinkProgram(program); int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); @@ -583,13 +704,6 @@ public class TextureRenderer { return program; } - public void checkGlError(String op) { - int error; - if ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { - throw new RuntimeException(op + ": glError " + error); - } - } - public void release() { if (mediaEntities != null) { for (int a = 0, N = mediaEntities.size(); a < N; a++) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/video/Track.java b/TMessagesProj/src/main/java/org/telegram/messenger/video/Track.java index 015714f84..e90f46e1e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/video/Track.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/video/Track.java @@ -328,6 +328,10 @@ public class Track { return samples; } + public long getLastFrameTimestamp() { + return ((duration - sampleDurations[sampleDurations.length - 1]) * 1000000 - 500000) / timeScale; + } + public long getDuration() { return duration; } diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java b/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java index 5c28868b1..2cdf3e845 100644 --- a/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java @@ -350,8 +350,17 @@ public class ConnectionsManager extends BaseController { native_setProxySettings(currentAccount, SharedConfig.currentProxy.address, SharedConfig.currentProxy.port, SharedConfig.currentProxy.username, SharedConfig.currentProxy.password, SharedConfig.currentProxy.secret); } + String installer = ""; + try { + installer = ApplicationLoader.applicationContext.getPackageManager().getInstallerPackageName(ApplicationLoader.applicationContext.getPackageName()); + } catch (Throwable ignore) { - native_init(currentAccount, version, layer, apiId, deviceModel, systemVersion, appVersion, langCode, systemLangCode, configPath, logPath, regId, cFingerprint, timezoneOffset, userId, enablePushConnection, ApplicationLoader.isNetworkOnline(), ApplicationLoader.getCurrentNetworkType()); + } + if (installer == null) { + installer = ""; + } + + native_init(currentAccount, version, layer, apiId, deviceModel, systemVersion, appVersion, langCode, systemLangCode, configPath, logPath, regId, cFingerprint, installer, timezoneOffset, userId, enablePushConnection, ApplicationLoader.isNetworkOnline(), ApplicationLoader.getCurrentNetworkType()); checkConnection(); } @@ -512,14 +521,6 @@ public class ConnectionsManager extends BaseController { EmuDetector detector = EmuDetector.with(ApplicationLoader.applicationContext); if (detector.detect()) { flags |= 1024; - } - try { - String installer = ApplicationLoader.applicationContext.getPackageManager().getInstallerPackageName(ApplicationLoader.applicationContext.getPackageName()); - if ("com.android.vending".equals(installer)) { - flags |= 2048; - } - } catch (Throwable ignore) { - } return flags; } @@ -533,16 +534,18 @@ public class ConnectionsManager extends BaseController { } public static void onRequestNewServerIpAndPort(final int second, final int currentAccount) { - Utilities.stageQueue.postRunnable(() -> { + Utilities.globalQueue.postRunnable(() -> { + boolean networkOnline = ApplicationLoader.isNetworkOnline(); + Utilities.stageQueue.postRunnable(() -> { - if (currentTask != null || second == 0 && Math.abs(lastDnsRequestTime - System.currentTimeMillis()) < 10000 || !ApplicationLoader.isNetworkOnline()) { - if (BuildVars.LOGS_ENABLED) { - FileLog.d("don't start task, current task = " + currentTask + " next task = " + second + " time diff = " + Math.abs(lastDnsRequestTime - System.currentTimeMillis()) + " network = " + ApplicationLoader.isNetworkOnline()); + if (currentTask != null || second == 0 && Math.abs(lastDnsRequestTime - System.currentTimeMillis()) < 10000 || !networkOnline) { + if (BuildVars.LOGS_ENABLED) { + FileLog.d("don't start task, current task = " + currentTask + " next task = " + second + " time diff = " + Math.abs(lastDnsRequestTime - System.currentTimeMillis()) + " network = " + ApplicationLoader.isNetworkOnline()); + } + return; } - return; - } - lastDnsRequestTime = System.currentTimeMillis(); + lastDnsRequestTime = System.currentTimeMillis(); if (BuildVars.LOGS_ENABLED) { FileLog.d("start dns txt task"); @@ -551,6 +554,7 @@ public class ConnectionsManager extends BaseController { task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); currentTask = task; + }); }); } @@ -679,7 +683,7 @@ public class ConnectionsManager extends BaseController { public static native void native_setUserId(int currentAccount, int id); - public static native void native_init(int currentAccount, int version, int layer, int apiId, String deviceModel, String systemVersion, String appVersion, String langCode, String systemLangCode, String configPath, String logPath, String regId, String cFingerprint, int timezoneOffset, int userId, boolean enablePushConnection, boolean hasNetwork, int networkType); + public static native void native_init(int currentAccount, int version, int layer, int apiId, String deviceModel, String systemVersion, String appVersion, String langCode, String systemLangCode, String configPath, String logPath, String regId, String cFingerprint, String installer, int timezoneOffset, int userId, boolean enablePushConnection, boolean hasNetwork, int networkType); public static native void native_setProxySettings(int currentAccount, String address, int port, String username, String password, String secret); diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java b/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java index b5d6de80e..1a3ed12dd 100644 --- a/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java @@ -61,7 +61,159 @@ public class TLRPC { public static final int MESSAGE_FLAG_EDITED = 0x00008000; public static final int MESSAGE_FLAG_MEGAGROUP = 0x80000000; - public static final int LAYER = 114; + public static final int LAYER = 116; + + public static class TL_stats_megagroupStats extends TLObject { + public static int constructor = 0xef7ff916; + + public TL_statsDateRangeDays period; + public TL_statsAbsValueAndPrev members; + public TL_statsAbsValueAndPrev messages; + public TL_statsAbsValueAndPrev viewers; + public TL_statsAbsValueAndPrev posters; + public StatsGraph growth_graph; + public StatsGraph members_graph; + public StatsGraph new_members_by_source_graph; + public StatsGraph languages_graph; + public StatsGraph messages_graph; + public StatsGraph actions_graph; + public StatsGraph top_hours_graph; + public StatsGraph weekdays_graph; + public ArrayList top_posters = new ArrayList<>(); + public ArrayList top_admins = new ArrayList<>(); + public ArrayList top_inviters = new ArrayList<>(); + public ArrayList users = new ArrayList<>(); + + public static TL_stats_megagroupStats TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_stats_megagroupStats.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_stats_megagroupStats", constructor)); + } else { + return null; + } + } + TL_stats_megagroupStats result = new TL_stats_megagroupStats(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + period = TL_statsDateRangeDays.TLdeserialize(stream, stream.readInt32(exception), exception); + members = TL_statsAbsValueAndPrev.TLdeserialize(stream, stream.readInt32(exception), exception); + messages = TL_statsAbsValueAndPrev.TLdeserialize(stream, stream.readInt32(exception), exception); + viewers = TL_statsAbsValueAndPrev.TLdeserialize(stream, stream.readInt32(exception), exception); + posters = TL_statsAbsValueAndPrev.TLdeserialize(stream, stream.readInt32(exception), exception); + growth_graph = StatsGraph.TLdeserialize(stream, stream.readInt32(exception), exception); + members_graph = StatsGraph.TLdeserialize(stream, stream.readInt32(exception), exception); + new_members_by_source_graph = StatsGraph.TLdeserialize(stream, stream.readInt32(exception), exception); + languages_graph = StatsGraph.TLdeserialize(stream, stream.readInt32(exception), exception); + messages_graph = StatsGraph.TLdeserialize(stream, stream.readInt32(exception), exception); + actions_graph = StatsGraph.TLdeserialize(stream, stream.readInt32(exception), exception); + top_hours_graph = StatsGraph.TLdeserialize(stream, stream.readInt32(exception), exception); + weekdays_graph = StatsGraph.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++) { + TL_statsGroupTopPoster object = TL_statsGroupTopPoster.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + top_posters.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++) { + TL_statsGroupTopAdmin object = TL_statsGroupTopAdmin.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + top_admins.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++) { + TL_statsGroupTopInviter object = TL_statsGroupTopInviter.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + top_inviters.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); + period.serializeToStream(stream); + members.serializeToStream(stream); + messages.serializeToStream(stream); + viewers.serializeToStream(stream); + posters.serializeToStream(stream); + growth_graph.serializeToStream(stream); + members_graph.serializeToStream(stream); + new_members_by_source_graph.serializeToStream(stream); + languages_graph.serializeToStream(stream); + messages_graph.serializeToStream(stream); + actions_graph.serializeToStream(stream); + top_hours_graph.serializeToStream(stream); + weekdays_graph.serializeToStream(stream); + stream.writeInt32(0x1cb5c415); + int count = top_posters.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + top_posters.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = top_admins.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + top_admins.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = top_inviters.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + top_inviters.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_chatBannedRights extends TLObject { public static int constructor = 0x9f120418; @@ -243,6 +395,8 @@ public class TLRPC { public static abstract class ChatPhoto extends TLObject { + public int flags; + public boolean has_video; public FileLocation photo_small; public FileLocation photo_big; public int dc_id; @@ -250,13 +404,16 @@ public class TLRPC { public static ChatPhoto TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { ChatPhoto result = null; switch (constructor) { + case 0x475cdbd5: + result = new TL_chatPhoto_layer115(); + break; case 0x37c1011c: result = new TL_chatPhotoEmpty(); break; case 0x6153276a: result = new TL_chatPhoto_layer97(); break; - case 0x475cdbd5: + case 0xd20b9f3c: result = new TL_chatPhoto(); break; } @@ -270,6 +427,24 @@ public class TLRPC { } } + public static class TL_chatPhoto_layer115 extends TL_chatPhoto { + public static int constructor = 0x475cdbd5; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + photo_small = FileLocation.TLdeserialize(stream, stream.readInt32(exception), exception); + photo_big = FileLocation.TLdeserialize(stream, stream.readInt32(exception), exception); + dc_id = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + photo_small.serializeToStream(stream); + photo_big.serializeToStream(stream); + stream.writeInt32(dc_id); + } + } + public static class TL_chatPhotoEmpty extends ChatPhoto { public static int constructor = 0x37c1011c; @@ -296,10 +471,12 @@ public class TLRPC { } public static class TL_chatPhoto extends ChatPhoto { - public static int constructor = 0x475cdbd5; + public static int constructor = 0xd20b9f3c; public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + has_video = (flags & 1) != 0; photo_small = FileLocation.TLdeserialize(stream, stream.readInt32(exception), exception); photo_big = FileLocation.TLdeserialize(stream, stream.readInt32(exception), exception); dc_id = stream.readInt32(exception); @@ -307,6 +484,8 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); + flags = has_video ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); photo_small.serializeToStream(stream); photo_big.serializeToStream(stream); stream.writeInt32(dc_id); @@ -2536,6 +2715,43 @@ public class TLRPC { } } + public static class TL_statsGroupTopAdmin extends TLObject { + public static int constructor = 0x6014f412; + + public int user_id; + public int deleted; + public int kicked; + public int banned; + + public static TL_statsGroupTopAdmin TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_statsGroupTopAdmin.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_statsGroupTopAdmin", constructor)); + } else { + return null; + } + } + TL_statsGroupTopAdmin result = new TL_statsGroupTopAdmin(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + deleted = stream.readInt32(exception); + kicked = stream.readInt32(exception); + banned = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + stream.writeInt32(deleted); + stream.writeInt32(kicked); + stream.writeInt32(banned); + } + } + public static class TL_payments_paymentForm extends TLObject { public static int constructor = 0x3f56aea3; @@ -4306,6 +4522,7 @@ public class TLRPC { public int participants_count; public ArrayList participants = new ArrayList<>(); public Chat chat; + public int expires; public static ChatInvite TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { ChatInvite result = null; @@ -4313,6 +4530,9 @@ public class TLRPC { case 0xdfc2f58e: result = new TL_chatInvite(); break; + case 0x61695cb0: + result = new TL_chatInvitePeek(); + break; case 0x5a686d7c: result = new TL_chatInviteAlready(); break; @@ -4380,6 +4600,22 @@ public class TLRPC { } } + public static class TL_chatInvitePeek extends ChatInvite { + public static int constructor = 0x61695cb0; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + chat = Chat.TLdeserialize(stream, stream.readInt32(exception), exception); + expires = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + chat.serializeToStream(stream); + stream.writeInt32(expires); + } + } + public static class TL_chatInviteAlready extends ChatInvite { public static int constructor = 0x5a686d7c; @@ -5118,7 +5354,7 @@ public class TLRPC { } public static class TL_peerSettings extends TLObject { - public static int constructor = 0x818426cd; + public static int constructor = 0x733f2961; public int flags; public boolean report_spam; @@ -5127,6 +5363,8 @@ public class TLRPC { public boolean share_contact; public boolean need_contacts_exception; public boolean report_geo; + public boolean autoarchived; + public int geo_distance; public static TL_peerSettings TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { if (TL_peerSettings.constructor != constructor) { @@ -5149,6 +5387,10 @@ public class TLRPC { share_contact = (flags & 8) != 0; need_contacts_exception = (flags & 16) != 0; report_geo = (flags & 32) != 0; + autoarchived = (flags & 128) != 0; + if ((flags & 64) != 0) { + geo_distance = stream.readInt32(exception); + } } public void serializeToStream(AbstractSerializedData stream) { @@ -5159,7 +5401,42 @@ public class TLRPC { flags = share_contact ? (flags | 8) : (flags &~ 8); flags = need_contacts_exception ? (flags | 16) : (flags &~ 16); flags = report_geo ? (flags | 32) : (flags &~ 32); + flags = autoarchived ? (flags | 128) : (flags &~ 128); stream.writeInt32(flags); + if ((flags & 64) != 0) { + stream.writeInt32(geo_distance); + } + } + } + + public static class TL_statsGroupTopInviter extends TLObject { + public static int constructor = 0x31962a4c; + + public int user_id; + public int invitations; + + public static TL_statsGroupTopInviter TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_statsGroupTopInviter.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_statsGroupTopInviter", constructor)); + } else { + return null; + } + } + TL_statsGroupTopInviter result = new TL_statsGroupTopInviter(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + invitations = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + stream.writeInt32(invitations); } } @@ -6954,6 +7231,41 @@ public class TLRPC { } } + public static class TL_globalPrivacySettings extends TLObject { + public static int constructor = 0xbea2f424; + + public int flags; + public boolean archive_and_mute_new_noncontact_peers; + + public static TL_globalPrivacySettings TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_globalPrivacySettings.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_globalPrivacySettings", constructor)); + } else { + return null; + } + } + TL_globalPrivacySettings result = new TL_globalPrivacySettings(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + if ((flags & 1) != 0) { + archive_and_mute_new_noncontact_peers = stream.readBool(exception); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + if ((flags & 1) != 0) { + stream.writeBool(archive_and_mute_new_noncontact_peers); + } + } + } + public static abstract class help_UserInfo extends TLObject { public static help_UserInfo TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { @@ -11491,27 +11803,39 @@ public class TLRPC { } } - public static class TL_videoSize extends TLObject { - public static int constructor = 0x435bb987; + public static abstract class VideoSize extends TLObject { + public int flags; public String type; public FileLocation location; public int w; public int h; public int size; + public double video_start_ts; - public static TL_videoSize TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - if (TL_videoSize.constructor != constructor) { - if (exception) { - throw new RuntimeException(String.format("can't parse magic %x in TL_videoSize", constructor)); - } else { - return null; - } + public static VideoSize TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + VideoSize result = null; + switch (constructor) { + case 0x435bb987: + result = new TL_videoSize_layer115(); + break; + case 0xe831c556: + result = new TL_videoSize(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in VideoSize", constructor)); + } + if (result != null) { + result.readParams(stream, exception); } - TL_videoSize result = new TL_videoSize(); - result.readParams(stream, exception); return result; } + } + + public static class TL_videoSize_layer115 extends TL_videoSize { + public static int constructor = 0x435bb987; + public void readParams(AbstractSerializedData stream, boolean exception) { type = stream.readString(exception); @@ -11531,6 +11855,36 @@ public class TLRPC { } } + public static class TL_videoSize extends VideoSize { + public static int constructor = 0xe831c556; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + type = stream.readString(exception); + location = FileLocation.TLdeserialize(stream, stream.readInt32(exception), exception); + w = stream.readInt32(exception); + h = stream.readInt32(exception); + size = stream.readInt32(exception); + if ((flags & 1) != 0) { + video_start_ts = stream.readDouble(exception); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + stream.writeString(type); + location.serializeToStream(stream); + stream.writeInt32(w); + stream.writeInt32(h); + stream.writeInt32(size); + if ((flags & 1) != 0) { + stream.writeDouble(video_start_ts); + } + } + } + public static abstract class BotInlineMessage extends TLObject { public int flags; public GeoPoint geo; @@ -15092,6 +15446,40 @@ public class TLRPC { } } + public static class TL_statsGroupTopPoster extends TLObject { + public static int constructor = 0x18f3d0f7; + + public int user_id; + public int messages; + public int avg_chars; + + public static TL_statsGroupTopPoster TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_statsGroupTopPoster.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_statsGroupTopPoster", constructor)); + } else { + return null; + } + } + TL_statsGroupTopPoster result = new TL_statsGroupTopPoster(); + result.readParams(stream, exception); + return result; + } + + public void readParams(AbstractSerializedData stream, boolean exception) { + user_id = stream.readInt32(exception); + messages = stream.readInt32(exception); + avg_chars = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(user_id); + stream.writeInt32(messages); + stream.writeInt32(avg_chars); + } + } + public static class TL_statsDateRangeDays extends TLObject { public static int constructor = 0xb637edaf; @@ -17775,7 +18163,7 @@ public class TLRPC { public String mime_type; public int size; public ArrayList thumbs = new ArrayList<>(); - public ArrayList video_thumbs = new ArrayList<>(); + public ArrayList video_thumbs = new ArrayList<>(); public int version; public int dc_id; public byte[] key; @@ -17994,7 +18382,7 @@ public class TLRPC { } int count = stream.readInt32(exception); for (int a = 0; a < count; a++) { - TL_videoSize object = TL_videoSize.TLdeserialize(stream, stream.readInt32(exception), exception); + VideoSize object = VideoSize.TLdeserialize(stream, stream.readInt32(exception), exception); if (object == null) { return; } @@ -19610,6 +19998,7 @@ public class TLRPC { public String venue_type; public int period; public boolean nosound_video; + public boolean force_file; public boolean stopped; public InputFile thumb; public String mime_type; @@ -19956,83 +20345,85 @@ public class TLRPC { } } - public static class TL_inputMediaUploadedDocument extends InputMedia { - public static int constructor = 0x5b38c6c1; + public static class TL_inputMediaUploadedDocument extends InputMedia { + public static int constructor = 0x5b38c6c1; - public void readParams(AbstractSerializedData stream, boolean exception) { - flags = stream.readInt32(exception); - nosound_video = (flags & 8) != 0; - file = InputFile.TLdeserialize(stream, stream.readInt32(exception), exception); - if ((flags & 4) != 0) { - thumb = InputFile.TLdeserialize(stream, stream.readInt32(exception), exception); - } - mime_type = stream.readString(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++) { - DocumentAttribute object = DocumentAttribute.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - attributes.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++) { - InputDocument object = InputDocument.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - stickers.add(object); - } - } - if ((flags & 2) != 0) { - ttl_seconds = stream.readInt32(exception); - } - } + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + nosound_video = (flags & 8) != 0; + force_file = (flags & 16) != 0; + file = InputFile.TLdeserialize(stream, stream.readInt32(exception), exception); + if ((flags & 4) != 0) { + thumb = InputFile.TLdeserialize(stream, stream.readInt32(exception), exception); + } + mime_type = stream.readString(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++) { + DocumentAttribute object = DocumentAttribute.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + attributes.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++) { + InputDocument object = InputDocument.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + stickers.add(object); + } + } + if ((flags & 2) != 0) { + ttl_seconds = stream.readInt32(exception); + } + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - flags = nosound_video ? (flags | 8) : (flags &~ 8); - stream.writeInt32(flags); - file.serializeToStream(stream); - if ((flags & 4) != 0) { - thumb.serializeToStream(stream); - } - stream.writeString(mime_type); - stream.writeInt32(0x1cb5c415); - int count = attributes.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - attributes.get(a).serializeToStream(stream); - } - if ((flags & 1) != 0) { - stream.writeInt32(0x1cb5c415); - count = stickers.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - stickers.get(a).serializeToStream(stream); - } - } - if ((flags & 2) != 0) { - stream.writeInt32(ttl_seconds); - } - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = nosound_video ? (flags | 8) : (flags &~ 8); + flags = force_file ? (flags | 16) : (flags &~ 16); + stream.writeInt32(flags); + file.serializeToStream(stream); + if ((flags & 4) != 0) { + thumb.serializeToStream(stream); + } + stream.writeString(mime_type); + stream.writeInt32(0x1cb5c415); + int count = attributes.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + attributes.get(a).serializeToStream(stream); + } + if ((flags & 1) != 0) { + stream.writeInt32(0x1cb5c415); + count = stickers.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + stickers.get(a).serializeToStream(stream); + } + } + if ((flags & 2) != 0) { + stream.writeInt32(ttl_seconds); + } + } + } public static class TL_inputMediaPhotoExternal extends InputMedia { public static int constructor = 0xe5bbfe1a; @@ -20862,6 +21253,9 @@ public class TLRPC { case 0xbec268ef: result = new TL_updateNotifySettings(); break; + case 0x65d2b464: + result = new TL_updateChannelParticipant(); + break; case 0x6e5f8c22: result = new TL_updateChatParticipantDelete(); break; @@ -20943,6 +21337,9 @@ public class TLRPC { case 0x26ffde7d: result = new TL_updateDialogFilter(); break; + case 0x2661bf09: + result = new TL_updatePhoneCallSignalingData(); + break; case 0xfa0f3ca2: result = new TL_updatePinnedDialogs(); break; @@ -21445,6 +21842,47 @@ public class TLRPC { } } + public static class TL_updateChannelParticipant extends Update { + public static int constructor = 0x65d2b464; + + public int flags; + public int channel_id; + public int date; + public int user_id; + public ChannelParticipant prev_participant; + public ChannelParticipant new_participant; + public int qts; + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + channel_id = stream.readInt32(exception); + date = stream.readInt32(exception); + user_id = stream.readInt32(exception); + if ((flags & 1) != 0) { + prev_participant = ChannelParticipant.TLdeserialize(stream, stream.readInt32(exception), exception); + } + if ((flags & 2) != 0) { + new_participant = ChannelParticipant.TLdeserialize(stream, stream.readInt32(exception), exception); + } + qts = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + stream.writeInt32(channel_id); + stream.writeInt32(date); + stream.writeInt32(user_id); + if ((flags & 1) != 0) { + prev_participant.serializeToStream(stream); + } + if ((flags & 2) != 0) { + new_participant.serializeToStream(stream); + } + stream.writeInt32(qts); + } + } + public static class TL_updateChatParticipantDelete extends Update { public static int constructor = 0x6e5f8c22; @@ -22067,6 +22505,24 @@ public class TLRPC { } } + public static class TL_updatePhoneCallSignalingData extends Update { + public static int constructor = 0x2661bf09; + + public long phone_call_id; + public byte[] data; + + public void readParams(AbstractSerializedData stream, boolean exception) { + phone_call_id = stream.readInt64(exception); + data = stream.readByteArray(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt64(phone_call_id); + stream.writeByteArray(data); + } + } + public static class TL_updatePinnedDialogs extends Update { public static int constructor = 0xfa0f3ca2; @@ -23259,6 +23715,8 @@ public class TLRPC { public static abstract class UserProfilePhoto extends TLObject { + public int flags; + public boolean has_video; public long photo_id; public FileLocation photo_small; public FileLocation photo_big; @@ -23267,9 +23725,12 @@ public class TLRPC { public static UserProfilePhoto TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { UserProfilePhoto result = null; switch (constructor) { - case 0xecd75d8c: + case 0x69d3ab26: result = new TL_userProfilePhoto(); break; + case 0xecd75d8c: + result = new TL_userProfilePhoto_layer115(); + break; case 0x4f11bae1: result = new TL_userProfilePhotoEmpty(); break; @@ -23291,6 +23752,30 @@ public class TLRPC { } public static class TL_userProfilePhoto extends UserProfilePhoto { + public static int constructor = 0x69d3ab26; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + has_video = (flags & 1) != 0; + photo_id = stream.readInt64(exception); + photo_small = FileLocation.TLdeserialize(stream, stream.readInt32(exception), exception); + photo_big = FileLocation.TLdeserialize(stream, stream.readInt32(exception), exception); + dc_id = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = has_video ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); + stream.writeInt64(photo_id); + photo_small.serializeToStream(stream); + photo_big.serializeToStream(stream); + stream.writeInt32(dc_id); + } + } + + public static class TL_userProfilePhoto_layer115 extends TL_userProfilePhoto { public static int constructor = 0xecd75d8c; @@ -23800,6 +24285,7 @@ public class TLRPC { public byte[] file_reference; public int date; public ArrayList sizes = new ArrayList<>(); + public ArrayList video_sizes = new ArrayList<>(); public int dc_id; public int user_id; public GeoPoint geo; @@ -23824,6 +24310,9 @@ public class TLRPC { result = new TL_photo_old(); break; case 0xd07504a5: + result = new TL_photo_layer115(); + break; + case 0xfb197a65: result = new TL_photo(); break; case 0x9288dd29: @@ -24026,7 +24515,7 @@ public class TLRPC { } } - public static class TL_photo extends Photo { + public static class TL_photo_layer115 extends TL_photo { public static int constructor = 0xd07504a5; @@ -24073,6 +24562,78 @@ public class TLRPC { } } + public static class TL_photo extends Photo { + public static int constructor = 0xfb197a65; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + has_stickers = (flags & 1) != 0; + id = stream.readInt64(exception); + access_hash = stream.readInt64(exception); + file_reference = stream.readByteArray(exception); + date = 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++) { + PhotoSize object = PhotoSize.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + sizes.add(object); + } + if ((flags & 2) != 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++) { + VideoSize object = VideoSize.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + video_sizes.add(object); + } + } + dc_id = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = has_stickers ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); + stream.writeInt64(id); + stream.writeInt64(access_hash); + stream.writeByteArray(file_reference); + stream.writeInt32(date); + stream.writeInt32(0x1cb5c415); + int count = sizes.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + sizes.get(a).serializeToStream(stream); + } + if ((flags & 2) != 0) { + stream.writeInt32(0x1cb5c415); + count = video_sizes.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + video_sizes.get(a).serializeToStream(stream); + } + } + stream.writeInt32(dc_id); + } + } + public static class TL_photo_layer82 extends TL_photo { public static int constructor = 0x9288dd29; @@ -24116,6 +24677,38 @@ public class TLRPC { } } + public static class TL_encryptedChatRequested extends EncryptedChat { + public static int constructor = 0x62718a82; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + if ((flags & 1) != 0) { + folder_id = stream.readInt32(exception); + } + id = stream.readInt32(exception); + access_hash = stream.readInt64(exception); + date = stream.readInt32(exception); + admin_id = stream.readInt32(exception); + participant_id = stream.readInt32(exception); + g_a = stream.readByteArray(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + if ((flags & 1) != 0) { + stream.writeInt32(folder_id); + } + stream.writeInt32(id); + stream.writeInt64(access_hash); + stream.writeInt32(date); + stream.writeInt32(admin_id); + stream.writeInt32(participant_id); + stream.writeByteArray(g_a); + } + } + public static class TL_encryptedChatRequested_old extends TL_encryptedChatRequested { public static int constructor = 0xfda9a7b7; @@ -24142,7 +24735,7 @@ public class TLRPC { } } - public static class TL_encryptedChatRequested extends EncryptedChat { + public static class TL_encryptedChatRequested_layer115 extends EncryptedChat { public static int constructor = 0xc878527e; @@ -32855,69 +33448,90 @@ public class TLRPC { } } - public static abstract class InputChatPhoto extends TLObject { - public InputPhoto id; - public InputFile file; + public static abstract class InputChatPhoto extends TLObject { - public static InputChatPhoto TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - InputChatPhoto result = null; - switch (constructor) { - case 0x8953ad37: - result = new TL_inputChatPhoto(); - break; - case 0x1ca48f57: - result = new TL_inputChatPhotoEmpty(); - break; - case 0x927c55b4: - result = new TL_inputChatUploadedPhoto(); - break; - } - if (result == null && exception) { - throw new RuntimeException(String.format("can't parse magic %x in InputChatPhoto", constructor)); - } - if (result != null) { - result.readParams(stream, exception); - } - return result; - } - } + public static InputChatPhoto TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + InputChatPhoto result = null; + switch (constructor) { + case 0x8953ad37: + result = new TL_inputChatPhoto(); + break; + case 0x1ca48f57: + result = new TL_inputChatPhotoEmpty(); + break; + case 0xc642724e: + result = new TL_inputChatUploadedPhoto(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in InputChatPhoto", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } - public static class TL_inputChatPhoto extends InputChatPhoto { - public static int constructor = 0x8953ad37; + public static class TL_inputChatPhoto extends InputChatPhoto { + public static int constructor = 0x8953ad37; + + public InputPhoto id; + + public void readParams(AbstractSerializedData stream, boolean exception) { + id = InputPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + id.serializeToStream(stream); + } + } + + public static class TL_inputChatPhotoEmpty extends InputChatPhoto { + public static int constructor = 0x1ca48f57; - public void readParams(AbstractSerializedData stream, boolean exception) { - id = InputPhoto.TLdeserialize(stream, stream.readInt32(exception), exception); - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - id.serializeToStream(stream); - } - } + public static class TL_inputChatUploadedPhoto extends InputChatPhoto { + public static int constructor = 0xc642724e; - public static class TL_inputChatPhotoEmpty extends InputChatPhoto { - public static int constructor = 0x1ca48f57; + public int flags; + public InputFile file; + public InputFile video; + public double video_start_ts; + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + if ((flags & 1) != 0) { + file = InputFile.TLdeserialize(stream, stream.readInt32(exception), exception); + } + if ((flags & 2) != 0) { + video = InputFile.TLdeserialize(stream, stream.readInt32(exception), exception); + } + if ((flags & 4) != 0) { + video_start_ts = stream.readDouble(exception); + } + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - } - } - - public static class TL_inputChatUploadedPhoto extends InputChatPhoto { - public static int constructor = 0x927c55b4; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - file = InputFile.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - file.serializeToStream(stream); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + if ((flags & 1) != 0) { + file.serializeToStream(stream); + } + if ((flags & 2) != 0) { + video.serializeToStream(stream); + } + if ((flags & 4) != 0) { + stream.writeDouble(video_start_ts); + } + } + } public static class TL_nearestDc extends TLObject { public static int constructor = 0x8e1a1775; @@ -35401,6 +36015,21 @@ public class TLRPC { } } + public static class TL_help_dismissSuggestion extends TLObject { + public static int constructor = 0x77fa99f; + + public String suggestion; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return Bool.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeString(suggestion); + } + } + public static class TL_messages_readHistory extends TLObject { public static int constructor = 0xe306d3a; @@ -36072,35 +36701,47 @@ public class TLRPC { } } - public static class TL_photos_updateProfilePhoto extends TLObject { - public static int constructor = 0xf0bb5152; + public static class TL_photos_updateProfilePhoto extends TLObject { + public static int constructor = 0x72d4742c; - public InputPhoto id; + public InputPhoto id; - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return UserProfilePhoto.TLdeserialize(stream, constructor, exception); - } + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_photos_photo.TLdeserialize(stream, constructor, exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - id.serializeToStream(stream); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + id.serializeToStream(stream); + } + } - public static class TL_photos_uploadProfilePhoto extends TLObject { - public static int constructor = 0x4f32c098; + public static class TL_photos_uploadProfilePhoto extends TLObject { + public static int constructor = 0x89f30f69; - public InputFile file; + public int flags; + public InputFile file; + public InputFile video; + public double video_start_ts; - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return TL_photos_photo.TLdeserialize(stream, constructor, exception); - } + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_photos_photo.TLdeserialize(stream, constructor, exception); + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - file.serializeToStream(stream); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + if ((flags & 1) != 0) { + file.serializeToStream(stream); + } + if ((flags & 2) != 0) { + video.serializeToStream(stream); + } + if ((flags & 4) != 0) { + stream.writeDouble(video_start_ts); + } + } + } public static class TL_photos_deletePhotos extends TLObject { public static int constructor = 0x87cf7f2f; @@ -36753,6 +37394,34 @@ public class TLRPC { } } + public static class TL_account_getGlobalPrivacySettings extends TLObject { + public static int constructor = 0xeb2b4cf6; + + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_globalPrivacySettings.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_account_setGlobalPrivacySettings extends TLObject { + public static int constructor = 0x1edaaac2; + + public TL_globalPrivacySettings settings; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_globalPrivacySettings.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + settings.serializeToStream(stream); + } + } + public static class TL_account_getAllSecureValues extends TLObject { public static int constructor = 0xb288bc7d; @@ -40748,6 +41417,25 @@ public class TLRPC { } } + public static class TL_stats_getMegagroupStats extends TLObject { + public static int constructor = 0xdcdf8607; + + public int flags; + public boolean dark; + public InputChannel channel; + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return TL_stats_megagroupStats.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = dark ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); + channel.serializeToStream(stream); + } + } + //manually created //RichText start @@ -41297,6 +41985,8 @@ public class TLRPC { //EncryptedChat start public static abstract class EncryptedChat extends TLObject { + public int flags; + public int folder_id; public int id; public long access_hash; public int date; @@ -41330,11 +42020,14 @@ public class TLRPC { result = new TL_encryptedChatRequested_old(); break; case 0xc878527e: - result = new TL_encryptedChatRequested(); + result = new TL_encryptedChatRequested_layer115(); break; case 0xfa56ce36: result = new TL_encryptedChat(); break; + case 0x62718a82: + result = new TL_encryptedChatRequested(); + break; case 0x6601d14f: result = new TL_encryptedChat_old(); break; 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 0d7116684..253101474 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java @@ -218,6 +218,8 @@ public class ActionBarLayout extends FrameLayout { private Runnable waitingForKeyboardCloseRunnable; private Runnable delayedOpenAnimationRunnable; + private boolean inBubbleMode; + private boolean inPreviewMode; private boolean previewOpenAnimationInProgress; private ColorDrawable previewBackgroundDrawable; @@ -336,6 +338,14 @@ public class ActionBarLayout extends FrameLayout { drawHeaderShadow(canvas, 255, y); } + public void setInBubbleMode(boolean value) { + inBubbleMode = value; + } + + public boolean isInBubbleMode() { + return inBubbleMode; + } + public void drawHeaderShadow(Canvas canvas, int alpha, int y) { if (headerShadowDrawable != null) { headerShadowDrawable.setAlpha(alpha); @@ -358,7 +368,7 @@ public class ActionBarLayout extends FrameLayout { public void dismissDialogs() { if (!fragmentsStack.isEmpty()) { BaseFragment lastFragment = fragmentsStack.get(fragmentsStack.size() - 1); - lastFragment.dismissCurrentDialig(); + lastFragment.dismissCurrentDialog(); } } @@ -583,7 +593,7 @@ public class ActionBarLayout extends FrameLayout { int dx = Math.max(0, (int) (ev.getX() - startedTrackingX)); int dy = Math.abs((int) ev.getY() - startedTrackingY); velocityTracker.addMovement(ev); - if (!inPreviewMode && maybeStartTracking && !startedTracking && dx >= AndroidUtilities.getPixelsInCM(0.4f, true) && Math.abs(dx) / 3 > dy) { + if (!transitionAnimationInProgress && !inPreviewMode && maybeStartTracking && !startedTracking && dx >= AndroidUtilities.getPixelsInCM(0.4f, true) && Math.abs(dx) / 3 > dy) { BaseFragment currentFragment = fragmentsStack.get(fragmentsStack.size() - 1); if (currentFragment.canBeginSlide()) { prepareForMoving(ev); @@ -1014,10 +1024,12 @@ public class ActionBarLayout extends FrameLayout { fragment.onTransitionAnimationEnd(true, false); fragment.onBecomeFullyVisible(); }; - if (currentFragment != null) { - currentFragment.onTransitionAnimationStart(false, false); + if (!fragment.needDelayOpenAnimation()) { + if (currentFragment != null) { + currentFragment.onTransitionAnimationStart(false, false); + } + fragment.onTransitionAnimationStart(true, false); } - fragment.onTransitionAnimationStart(true, false); oldFragment = currentFragment; newFragment = fragment; AnimatorSet animation = null; @@ -1055,6 +1067,10 @@ public class ActionBarLayout extends FrameLayout { return; } delayedOpenAnimationRunnable = null; + if (currentFragment != null) { + currentFragment.onTransitionAnimationStart(false, false); + } + fragment.onTransitionAnimationStart(true, false); startLayoutAnimation(true, true, preview); } }; @@ -1744,13 +1760,9 @@ public class ActionBarLayout extends FrameLayout { onOpenAnimationEnd(); } containerView.invalidate(); - if (intent != null) { - parentActivity.startActivityForResult(intent, requestCode); - } - } else { - if (intent != null) { - parentActivity.startActivityForResult(intent, requestCode); - } + } + if (intent != null) { + parentActivity.startActivityForResult(intent, requestCode); } } 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 8f73ae0d8..0009b17db 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java @@ -122,6 +122,7 @@ public class ActionBarMenuItem extends FrameLayout { private boolean clearsTextOnSearchCollapse = true; private boolean measurePopup = true; private boolean forceSmoothKeyboard; + private boolean showSubmenuByMove = true; @Override @@ -153,6 +154,7 @@ public class ActionBarMenuItem extends FrameLayout { textView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); textView.setGravity(Gravity.CENTER); textView.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4), 0); + textView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); if (iconColor != 0) { textView.setTextColor(iconColor); } @@ -160,6 +162,7 @@ public class ActionBarMenuItem extends FrameLayout { } else { iconView = new ImageView(context); iconView.setScaleType(ImageView.ScaleType.CENTER); + iconView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); addView(iconView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); if (iconColor != 0) { iconView.setColorFilter(new PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_IN)); @@ -184,7 +187,7 @@ public class ActionBarMenuItem extends FrameLayout { AndroidUtilities.runOnUIThread(showMenuRunnable, 200); } } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { - if (hasSubMenu() && (popupWindow == null || popupWindow != null && !popupWindow.isShowing())) { + if (showSubmenuByMove && hasSubMenu() && (popupWindow == null || popupWindow != null && !popupWindow.isShowing())) { if (event.getY() > getHeight()) { if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); @@ -253,6 +256,10 @@ public class ActionBarMenuItem extends FrameLayout { subMenuDelegate = actionBarSubMenuItemDelegate; } + public void setShowSubmenuByMove(boolean value) { + showSubmenuByMove = value; + } + public void setIconColor(int color) { if (iconView != null) { iconView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); @@ -309,6 +316,13 @@ public class ActionBarMenuItem extends FrameLayout { popupLayout.removeInnerViews(); } + public void setShowedFromBottom(boolean value) { + if (popupLayout == null) { + return; + } + popupLayout.setShowedFromBotton(value); + } + public void addSubItem(View view, int width, int height) { createPopupLayout(); popupLayout.addView(view, new LinearLayout.LayoutParams(width, height)); @@ -383,9 +397,13 @@ public class ActionBarMenuItem extends FrameLayout { } public ActionBarMenuSubItem addSubItem(int id, int icon, CharSequence text) { + return addSubItem(id, icon, text, false); + } + + public ActionBarMenuSubItem addSubItem(int id, int icon, CharSequence text, boolean needCheck) { createPopupLayout(); - ActionBarMenuSubItem cell = new ActionBarMenuSubItem(getContext()); + ActionBarMenuSubItem cell = new ActionBarMenuSubItem(getContext(), needCheck); cell.setTextAndIcon(text, icon); cell.setMinimumWidth(AndroidUtilities.dp(196)); cell.setTag(id); @@ -1144,6 +1162,13 @@ public class ActionBarMenuItem extends FrameLayout { @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); - info.setClassName("android.widget.ImageButton"); + if (iconView != null) { + info.setClassName("android.widget.ImageButton"); + } else if (textView != null) { + info.setClassName("android.widget.Button"); + if (TextUtils.isEmpty(info.getText())) { + info.setText(textView.getText()); + } + } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuSubItem.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuSubItem.java index 304f1bb39..fb0ffdf40 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuSubItem.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuSubItem.java @@ -13,18 +13,24 @@ import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; import org.telegram.ui.Components.LayoutHelper; public class ActionBarMenuSubItem extends FrameLayout { private TextView textView; private ImageView imageView; + private ImageView checkView; private int textColor = Theme.getColor(Theme.key_actionBarDefaultSubmenuItem); private int iconColor = Theme.getColor(Theme.key_actionBarDefaultSubmenuItemIcon); private int selectorColor = Theme.getColor(Theme.key_dialogButtonSelector); public ActionBarMenuSubItem(Context context) { + this(context, false); + } + + public ActionBarMenuSubItem(Context context, boolean needCheck) { super(context); setBackground(Theme.createSelectorDrawable(selectorColor, 2)); @@ -38,11 +44,19 @@ public class ActionBarMenuSubItem extends FrameLayout { textView = new TextView(context); textView.setLines(1); textView.setSingleLine(true); - textView.setGravity(Gravity.CENTER_HORIZONTAL); + textView.setGravity(Gravity.LEFT); textView.setEllipsize(TextUtils.TruncateAt.END); textView.setTextColor(textColor); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); addView(textView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL)); + + if (needCheck) { + checkView = new ImageView(context); + checkView.setImageResource(R.drawable.msg_text_check); + checkView.setScaleType(ImageView.ScaleType.CENTER); + checkView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_radioBackgroundChecked), PorterDuff.Mode.MULTIPLY)); + addView(checkView, LayoutHelper.createFrame(26, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); + } } @Override @@ -50,6 +64,26 @@ public class ActionBarMenuSubItem extends FrameLayout { super.onMeasure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(48), View.MeasureSpec.EXACTLY)); } + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (checkView != null) { + if (LocaleController.isRTL) { + left = getPaddingRight(); + } else { + left = getMeasuredWidth() - checkView.getMeasuredWidth() - getPaddingLeft(); + } + checkView.layout(left, checkView.getTop(), left + checkView.getMeasuredWidth(), checkView.getBottom()); + } + } + + public void setChecked(boolean checked) { + if (checkView == null) { + return; + } + checkView.setVisibility(checked ? VISIBLE : INVISIBLE); + } + public void setTextAndIcon(CharSequence text, int icon) { textView.setText(text); if (icon != 0) { @@ -87,6 +121,10 @@ public class ActionBarMenuSubItem extends FrameLayout { textView.setText(text); } + public TextView getTextView() { + return textView; + } + public void setSelectorColor(int selectorColor) { if (this.selectorColor != selectorColor) { setBackground(Theme.createSelectorDrawable(this.selectorColor = selectorColor, 2)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarPopupWindow.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarPopupWindow.java index 9dba3b30c..7fd6e3257 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarPopupWindow.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarPopupWindow.java @@ -287,6 +287,14 @@ public class ActionBarPopupWindow extends PopupWindow { scrollView.scrollTo(0, 0); } } + + public void setupRadialSelectors(int color) { + int count = linearLayout.getChildCount(); + for (int a = 0; a < count; a++) { + View child = linearLayout.getChildAt(a); + child.setBackground(Theme.createRadSelectorDrawable(color, a == 0 ? 6 : 0, a == count - 1 ? 6 : 0)); + } + } } public ActionBarPopupWindow() { 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 969765303..1202f13dc 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java @@ -57,6 +57,7 @@ public class BaseFragment { protected ActionBarLayout parentLayout; protected ActionBar actionBar; protected boolean inPreviewMode; + protected boolean inBubbleMode; protected int classGuid; protected Bundle arguments; protected boolean hasOwnBackground = false; @@ -106,6 +107,14 @@ public class BaseFragment { return true; } + public void setInBubbleMode(boolean value) { + inBubbleMode = value; + } + + public boolean isInBubbleMode() { + return inBubbleMode; + } + protected void setInPreviewMode(boolean value) { inPreviewMode = value; if (actionBar != null) { @@ -163,6 +172,7 @@ public class BaseFragment { protected void setParentLayout(ActionBarLayout layout) { if (parentLayout != layout) { parentLayout = layout; + inBubbleMode = parentLayout != null && parentLayout.isInBubbleMode(); if (fragmentView != null) { ViewGroup parent = (ViewGroup) fragmentView.getParent(); if (parent != null) { @@ -207,7 +217,7 @@ public class BaseFragment { actionBar.setItemsBackgroundColor(Theme.getColor(Theme.key_actionBarActionModeDefaultSelector), true); actionBar.setItemsColor(Theme.getColor(Theme.key_actionBarDefaultIcon), false); actionBar.setItemsColor(Theme.getColor(Theme.key_actionBarActionModeDefaultIcon), true); - if (inPreviewMode) { + if (inPreviewMode || inBubbleMode) { actionBar.setOccupyStatusBar(false); } return actionBar; @@ -351,7 +361,7 @@ public class BaseFragment { } } - public void dismissCurrentDialig() { + public void dismissCurrentDialog() { if (visibleDialog == null) { return; } 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 61a0d7097..28f7bf57c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java @@ -552,6 +552,11 @@ public class BottomSheet extends Dialog { boolean canDismiss(); } + public void setCalcMandatoryInsets(boolean value) { + calcMandatoryInsets = value; + drawNavigationBar = value; + } + public static class BottomSheetDelegate implements BottomSheetDelegateInterface { @Override public void onOpenAnimationStart() { 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 e736730e6..2f5a13ee6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java @@ -32,7 +32,6 @@ import android.view.WindowInsets; import android.view.accessibility.AccessibilityEvent; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; -import android.widget.ListView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.BuildVars; @@ -93,13 +92,16 @@ public class DrawerLayoutContainer extends FrameLayout { if (Build.VERSION.SDK_INT >= 21) { setFitsSystemWindows(true); setOnApplyWindowInsetsListener((v, insets) -> { - final DrawerLayoutContainer drawerLayout = (DrawerLayoutContainer) v; + final DrawerLayoutContainer drawerLayoutContainer = (DrawerLayoutContainer) v; if (AndroidUtilities.statusBarHeight != insets.getSystemWindowInsetTop()) { - drawerLayout.requestLayout(); + drawerLayoutContainer.requestLayout(); + } + int newTopInset = insets.getSystemWindowInsetTop(); + if ((newTopInset != 0 || AndroidUtilities.isInMultiwindow) && AndroidUtilities.statusBarHeight != newTopInset) { + AndroidUtilities.statusBarHeight = newTopInset; } - AndroidUtilities.statusBarHeight = insets.getSystemWindowInsetTop(); lastInsets = insets; - drawerLayout.setWillNotDraw(insets.getSystemWindowInsetTop() <= 0 && getBackground() == null); + drawerLayoutContainer.setWillNotDraw(insets.getSystemWindowInsetTop() <= 0 && getBackground() == null); if (Build.VERSION.SDK_INT >= 28) { DisplayCutout cutout = insets.getDisplayCutout(); @@ -161,6 +163,9 @@ public class DrawerLayoutContainer extends FrameLayout { @Keep public void setDrawerPosition(float value) { + if (drawerLayout == null) { + return; + } drawerPosition = value; if (drawerPosition > drawerLayout.getMeasuredWidth()) { drawerPosition = drawerLayout.getMeasuredWidth(); @@ -189,7 +194,7 @@ public class DrawerLayoutContainer extends FrameLayout { } public void openDrawer(boolean fast) { - if (!allowOpenDrawer) { + if (!allowOpenDrawer || drawerLayout == null) { return; } if (AndroidUtilities.isTablet() && parentActionBarLayout != null && parentActionBarLayout.parentActivity != null) { @@ -215,6 +220,9 @@ public class DrawerLayoutContainer extends FrameLayout { } public void closeDrawer(boolean fast) { + if (drawerLayout == null) { + return; + } cancelCurrentAnimation(); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( @@ -239,11 +247,6 @@ public class DrawerLayoutContainer extends FrameLayout { startedTracking = false; currentAnimation = null; drawerOpened = opened; - if (!opened) { - if (drawerLayout instanceof ListView) { - ((ListView) drawerLayout).setSelectionFromTop(0, 0); - } - } if (Build.VERSION.SDK_INT >= 19) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); @@ -309,7 +312,7 @@ public class DrawerLayoutContainer extends FrameLayout { } public boolean onTouchEvent(MotionEvent ev) { - if (!parentActionBarLayout.checkTransitionAnimation()) { + if (drawerLayout != null && !parentActionBarLayout.checkTransitionAnimation()) { if (drawerOpened && ev != null && ev.getX() > drawerPosition && !startedTracking) { if (ev.getAction() == MotionEvent.ACTION_UP) { closeDrawer(false); 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 f41c8d7b9..24a24a9c8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java @@ -83,6 +83,7 @@ import org.telegram.ui.Cells.ThemesHorizontalListCell; import org.telegram.ui.Components.AudioVisualizerDrawable; import org.telegram.ui.Components.BackgroundGradientDrawable; import org.telegram.ui.Components.CombinedDrawable; +import org.telegram.ui.Components.PathAnimator; import org.telegram.ui.Components.RLottieDrawable; import org.telegram.ui.Components.ScamDrawable; import org.telegram.ui.Components.SvgHelper; @@ -1990,6 +1991,7 @@ public class Theme { public static TextPaint dialogs_onlinePaint; public static TextPaint dialogs_offlinePaint; public static Drawable dialogs_checkDrawable; + public static Drawable dialogs_playDrawable; public static Drawable dialogs_checkReadDrawable; public static Drawable dialogs_halfCheckDrawable; public static Drawable dialogs_clockDrawable; @@ -2083,6 +2085,7 @@ public class Theme { public static MessageDrawable chat_msgOutMediaDrawable; public static MessageDrawable chat_msgOutMediaSelectedDrawable; + public static PathAnimator playPauseAnimator; public static Drawable chat_msgOutCheckDrawable; public static Drawable chat_msgOutCheckSelectedDrawable; public static Drawable chat_msgOutCheckReadDrawable; @@ -2859,8 +2862,6 @@ public class Theme { public static final String key_player_progressBackground2 = "player_progressBackground2"; public static final String key_player_progressCachedBackground = "key_player_progressCachedBackground"; public static final String key_player_progress = "player_progress"; - public static final String key_player_placeholder = "player_placeholder"; - public static final String key_player_placeholderBackground = "player_placeholderBackground"; public static final String key_player_button = "player_button"; public static final String key_player_buttonActive = "player_buttonActive"; @@ -2885,6 +2886,7 @@ public class Theme { public final static String key_statisticChartLine_lightgreen = "statisticChartLine_lightgreen"; public final static String key_statisticChartLine_orange = "statisticChartLine_orange"; public final static String key_statisticChartLine_indigo = "statisticChartLine_indigo"; + public final static String key_statisticChartLineEmpty = "statisticChartLineEmpty"; private static HashSet myMessagesColorKeys = new HashSet<>(); private static HashMap defaultColors = new HashMap<>(); @@ -3457,12 +3459,10 @@ public class Theme { defaultColors.put(key_player_actionBarItems, 0xff8a8a8a); defaultColors.put(key_player_background, 0xffffffff); defaultColors.put(key_player_time, 0xff8c9296); - defaultColors.put(key_player_progressBackground, 0xffe9eff5); + defaultColors.put(key_player_progressBackground, 0xffEBEDF0); defaultColors.put(key_player_progressBackground2, 0xffCCD3DB); - defaultColors.put(key_player_progressCachedBackground, 0xffe9eff5); - defaultColors.put(key_player_progress, 0xff4b9fe3); - defaultColors.put(key_player_placeholder, 0xffa8a8a8); - defaultColors.put(key_player_placeholderBackground, 0xfff0f0f0); + defaultColors.put(key_player_progressCachedBackground, 0xffC5DCF0); + defaultColors.put(key_player_progress, 0xff54AAEB); defaultColors.put(key_player_button, 0xff333333); defaultColors.put(key_player_buttonActive, 0xff4ca8ea); @@ -3606,6 +3606,7 @@ public class Theme { defaultColors.put(key_statisticChartLine_lightgreen, 0xff8FCF39); defaultColors.put(key_statisticChartLine_orange, 0xffE3B727); defaultColors.put(key_statisticChartLine_indigo, 0xff7F79F3); + defaultColors.put(key_statisticChartLineEmpty, 0xFFEEEEEE); fallbackKeys.put(key_chat_adminText, key_chat_inTimeText); fallbackKeys.put(key_chat_adminSelectedText, key_chat_inTimeSelectedText); @@ -3744,6 +3745,14 @@ public class Theme { themeAccentExclusionKeys.add(key_chat_attachGalleryText); themeAccentExclusionKeys.add(key_chat_shareBackground); themeAccentExclusionKeys.add(key_chat_shareBackgroundSelected); + themeAccentExclusionKeys.add(key_statisticChartLine_blue); + themeAccentExclusionKeys.add(key_statisticChartLine_green); + themeAccentExclusionKeys.add(key_statisticChartLine_red); + themeAccentExclusionKeys.add(key_statisticChartLine_golden); + themeAccentExclusionKeys.add(key_statisticChartLine_lightblue); + themeAccentExclusionKeys.add(key_statisticChartLine_lightgreen); + themeAccentExclusionKeys.add(key_statisticChartLine_orange); + themeAccentExclusionKeys.add(key_statisticChartLine_indigo); myMessagesColorKeys.add(key_chat_outGreenCall); myMessagesColorKeys.add(key_chat_outBubble); @@ -4590,18 +4599,26 @@ public class Theme { public static Drawable getSelectorDrawable(int color, boolean whiteBackground) { if (whiteBackground) { + return getSelectorDrawable(color, key_windowBackgroundWhite); + } else { + return createSelectorDrawable(color, 2); + } + } + + public static Drawable getSelectorDrawable(int color, String backgroundColor) { + if (backgroundColor != null) { if (Build.VERSION.SDK_INT >= 21) { Drawable maskDrawable = new ColorDrawable(0xffffffff); ColorStateList colorStateList = new ColorStateList( new int[][]{StateSet.WILD_CARD}, new int[]{color} ); - return new RippleDrawable(colorStateList, new ColorDrawable(getColor(key_windowBackgroundWhite)), maskDrawable); + return new RippleDrawable(colorStateList, new ColorDrawable(getColor(backgroundColor)), maskDrawable); } else { StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(color)); stateListDrawable.addState(new int[]{android.R.attr.state_selected}, new ColorDrawable(color)); - stateListDrawable.addState(StateSet.WILD_CARD, new ColorDrawable(getColor(key_windowBackgroundWhite))); + stateListDrawable.addState(StateSet.WILD_CARD, new ColorDrawable(getColor(backgroundColor))); return stateListDrawable; } } else { @@ -4691,6 +4708,54 @@ public class Theme { } } + public static Drawable createRadSelectorDrawable(int color, int topRad, int bottomRad) { + Drawable drawable; + if (Build.VERSION.SDK_INT >= 21) { + maskPaint.setColor(0xffffffff); + Drawable maskDrawable = new Drawable() { + + private Path path = new Path(); + private RectF rect = new RectF(); + private float[] radii = new float[8]; + + @Override + public void draw(Canvas canvas) { + radii[0] = radii[1] = radii[2] = radii[3] = AndroidUtilities.dp(topRad); + radii[4] = radii[5] = radii[6] = radii[7] = AndroidUtilities.dp(bottomRad); + rect.set(getBounds()); + path.addRoundRect(rect, radii, Path.Direction.CW); + canvas.drawPath(path, maskPaint); + } + + @Override + public void setAlpha(int alpha) { + + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + + } + + @Override + public int getOpacity() { + return PixelFormat.UNKNOWN; + } + }; + ColorStateList colorStateList = new ColorStateList( + new int[][]{StateSet.WILD_CARD}, + new int[]{color} + ); + return new RippleDrawable(colorStateList, null, maskDrawable); + } else { + StateListDrawable stateListDrawable = new StateListDrawable(); + stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(color)); + stateListDrawable.addState(new int[]{android.R.attr.state_selected}, new ColorDrawable(color)); + stateListDrawable.addState(StateSet.WILD_CARD, new ColorDrawable(0x00000000)); + return stateListDrawable; + } + } + public static void applyPreviousTheme() { if (previousTheme == null) { return; @@ -6544,6 +6609,7 @@ public class Theme { dialogs_lockDrawable = resources.getDrawable(R.drawable.list_secret); dialogs_checkDrawable = resources.getDrawable(R.drawable.list_check).mutate(); + dialogs_playDrawable = resources.getDrawable(R.drawable.minithumb_play).mutate(); dialogs_checkReadDrawable = resources.getDrawable(R.drawable.list_check).mutate(); dialogs_halfCheckDrawable = resources.getDrawable(R.drawable.list_halfcheck); dialogs_clockDrawable = resources.getDrawable(R.drawable.deproko_baseline_clock_24).mutate(); @@ -6731,6 +6797,15 @@ public class Theme { chat_msgOutMediaDrawable = new MessageDrawable(MessageDrawable.TYPE_MEDIA, true, false); chat_msgOutMediaSelectedDrawable = new MessageDrawable(MessageDrawable.TYPE_MEDIA, true, true); + playPauseAnimator = new PathAnimator(0.293f, -26, -28); + playPauseAnimator.addSvgKeyFrame("M 34.141 16.042 C 37.384 17.921 40.886 20.001 44.211 21.965 C 46.139 23.104 49.285 24.729 49.586 25.917 C 50.289 28.687 48.484 30 46.274 30 L 6 30.021 C 3.79 30.021 2.075 30.023 2 26.021 L 2.009 3.417 C 2.009 0.417 5.326 -0.58 7.068 0.417 C 10.545 2.406 25.024 10.761 34.141 16.042 Z", 166); + playPauseAnimator.addSvgKeyFrame("M 37.843 17.769 C 41.143 19.508 44.131 21.164 47.429 23.117 C 48.542 23.775 49.623 24.561 49.761 25.993 C 50.074 28.708 48.557 30 46.347 30 L 6 30.012 C 3.79 30.012 2 28.222 2 26.012 L 2.009 4.609 C 2.009 1.626 5.276 0.664 7.074 1.541 C 10.608 3.309 28.488 12.842 37.843 17.769 Z", 200); + playPauseAnimator.addSvgKeyFrame("M 40.644 18.756 C 43.986 20.389 49.867 23.108 49.884 25.534 C 49.897 27.154 49.88 24.441 49.894 26.059 C 49.911 28.733 48.6 30 46.39 30 L 6 30.013 C 3.79 30.013 2 28.223 2 26.013 L 2.008 5.52 C 2.008 2.55 5.237 1.614 7.079 2.401 C 10.656 4 31.106 14.097 40.644 18.756 Z", 217); + playPauseAnimator.addSvgKeyFrame("M 43.782 19.218 C 47.117 20.675 50.075 21.538 50.041 24.796 C 50.022 26.606 50.038 24.309 50.039 26.104 C 50.038 28.736 48.663 30 46.453 30 L 6 29.986 C 3.79 29.986 2 28.196 2 25.986 L 2.008 6.491 C 2.008 3.535 5.196 2.627 7.085 3.316 C 10.708 4.731 33.992 14.944 43.782 19.218 Z", 234); + playPauseAnimator.addSvgKeyFrame("M 47.421 16.941 C 50.544 18.191 50.783 19.91 50.769 22.706 C 50.761 24.484 50.76 23.953 50.79 26.073 C 50.814 27.835 49.334 30 47.124 30 L 5 30.01 C 2.79 30.01 1 28.22 1 26.01 L 1.001 10.823 C 1.001 8.218 3.532 6.895 5.572 7.26 C 7.493 8.01 47.421 16.941 47.421 16.941 Z", 284); + playPauseAnimator.addSvgKeyFrame("M 47.641 17.125 C 50.641 18.207 51.09 19.935 51.078 22.653 C 51.07 24.191 51.062 21.23 51.088 23.063 C 51.109 24.886 49.587 27 47.377 27 L 5 27.009 C 2.79 27.009 1 25.219 1 23.009 L 0.983 11.459 C 0.983 8.908 3.414 7.522 5.476 7.838 C 7.138 8.486 47.641 17.125 47.641 17.125 Z", 300); + playPauseAnimator.addSvgKeyFrame("M 48 7 C 50.21 7 52 8.79 52 11 C 52 19 52 19 52 19 C 52 21.21 50.21 23 48 23 L 4 23 C 1.79 23 0 21.21 0 19 L 0 11 C 0 8.79 1.79 7 4 7 C 48 7 48 7 48 7 Z", 400); + chat_msgOutCheckDrawable = resources.getDrawable(R.drawable.msg_check).mutate(); chat_msgOutCheckSelectedDrawable = resources.getDrawable(R.drawable.msg_check).mutate(); chat_msgOutCheckReadDrawable = resources.getDrawable(R.drawable.msg_check).mutate(); 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 8a3b65e9c..3efb81550 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java @@ -924,6 +924,9 @@ public class DialogsSearchAdapter extends RecyclerListView.SelectionAdapter { notifyDataSetChanged(); } final int searchId = ++lastSearchId; + if (needMessagesSearch != 2 && delegate != null) { + delegate.searchStateChanged(true); + } Utilities.searchQueue.postRunnable(searchRunnable = () -> { searchRunnable = null; searchDialogsInternal(query, searchId); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DrawerLayoutAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DrawerLayoutAdapter.java index 34cc1b30a..2a1b796a9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DrawerLayoutAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DrawerLayoutAdapter.java @@ -19,7 +19,6 @@ import androidx.recyclerview.widget.RecyclerView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; -import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; @@ -277,9 +276,9 @@ public class DrawerLayoutAdapter extends RecyclerListView.SelectionAdapter imple } if (NekoXConfig.disableStatusUpdate && !UserConfig.getInstance(UserConfig.selectedAccount).isBot) { boolean online = MessagesController.getInstance(UserConfig.selectedAccount).isOnline(); - String message = online ? StrUtil.upperFirst(LocaleController.getString("Online", R.string.Online)) : LocaleController.getString("VoipOfflineTitle",R.string.VoipOfflineTitle); + String message = online ? StrUtil.upperFirst(LocaleController.getString("Online", R.string.Online)) : LocaleController.getString("VoipOfflineTitle", R.string.VoipOfflineTitle); if (NekoXConfig.keepOnlineStatus) { - message += " (" + LocaleController.getString("Locked",R.string.Locked) + ")"; + message += " (" + LocaleController.getString("Locked", R.string.Locked) + ")"; } items.add(new CheckItem(14, message, R.drawable.baseline_visibility_24, () -> online, () -> { MessagesController controller = MessagesController.getInstance(UserConfig.selectedAccount); @@ -315,7 +314,7 @@ public class DrawerLayoutAdapter extends RecyclerListView.SelectionAdapter imple return item instanceof CheckItem ? (CheckItem) item : null; } - public class Item { + private static class Item { public int icon; public String text; public int id; @@ -372,4 +371,4 @@ public class DrawerLayoutAdapter extends RecyclerListView.SelectionAdapter imple .apply(); notifyItemMoved(currentAdapterPosition, targetAdapterPosition); } -} +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/StickersAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/StickersAdapter.java index 9c02b8778..e06204b12 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/StickersAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/StickersAdapter.java @@ -417,7 +417,7 @@ public class StickersAdapter extends RecyclerListView.SelectionAdapter implement public Object getItem(int i) { if (keywordResults != null && !keywordResults.isEmpty()) { - return keywordResults.get(i).emoji; + return i >= 0 && i < keywordResults.size() ? keywordResults.get(i).emoji : null; } return stickers != null && i >= 0 && i < stickers.size() ? stickers.get(i).sticker : null; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ArticleViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/ArticleViewer.java index ef83e1718..a55d5e82f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ArticleViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ArticleViewer.java @@ -546,7 +546,16 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg @Override public void onSeekBarPressed(boolean pressed) { + } + @Override + public CharSequence getContentDescription() { + return String.valueOf(Math.round(startFontSize + (endFontSize - startFontSize) * sizeBar.getProgress())); + } + + @Override + public int getStepsCount() { + return endFontSize - startFontSize; } }); addView(sizeBar, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 38, Gravity.LEFT | Gravity.TOP, 5, 5, 39, 0)); @@ -612,8 +621,17 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg public void setTextAndTypeface(String text, Typeface typeface) { textView.setText(text); textView.setTypeface(typeface); + setContentDescription(text); invalidate(); } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(RadioButton.class.getName()); + info.setChecked(radioButton.isChecked()); + info.setCheckable(true); + } } private class FrameLayoutDrawer extends FrameLayout { @@ -1770,7 +1788,6 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg textSelectionHelper.clear(); } showDialog(linkSheet = builder.create()); - return true; } else { if (row < 0 || row >= adapter[0].blocks.size()) { return false; @@ -1805,8 +1822,8 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg offset = 0; } layoutManager[0].scrollToPositionWithOffset(row, currentHeaderHeight - AndroidUtilities.dp(56) - offset); - return true; } + return true; } return false; } @@ -2336,23 +2353,21 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg if (richText == pageBlockVideo.caption.text) { currentMap = mediaCaptionTextPaints; textSize = AndroidUtilities.dp(14); - textColor = getTextColor(); } else { currentMap = mediaCreditTextPaints; textSize = AndroidUtilities.dp(12); - textColor = getTextColor(); } + textColor = getTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockAudio) { TLRPC.TL_pageBlockAudio pageBlockAudio = (TLRPC.TL_pageBlockAudio) parentBlock; if (richText == pageBlockAudio.caption.text) { currentMap = mediaCaptionTextPaints; textSize = AndroidUtilities.dp(14); - textColor = getTextColor(); } else { currentMap = mediaCreditTextPaints; textSize = AndroidUtilities.dp(12); - textColor = getTextColor(); } + textColor = getTextColor(); } else if (parentBlock instanceof TLRPC.TL_pageBlockRelatedArticles) { currentMap = relatedArticleTextPaints; textSize = AndroidUtilities.dp(15); @@ -3960,6 +3975,16 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } setImageIndex(index, true); } + + @Override + public void onShowAnimationStart() { + + } + + @Override + public void onStopScrolling() { + + } }); captionTextViewNext = new TextView(activity); @@ -4438,7 +4463,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } private void openPreviewsChat(TLRPC.User user, long wid) { - if (user == null || parentActivity == null) { + if (user == null || !(parentActivity instanceof LaunchActivity)) { return; } Bundle args = new Bundle(); @@ -6586,7 +6611,6 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg buttonState = -1; } radialProgress.setIcon(getIconForCurrentState(), false, animated); - invalidate(); } else { DownloadController.getInstance(currentAccount).addLoadingFileObserver(fileName, null, this); float setProgress = 0; @@ -6606,8 +6630,8 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } radialProgress.setIcon(getIconForCurrentState(), progressVisible, animated); radialProgress.setProgress(setProgress, false); - invalidate(); } + invalidate(); } private void didPressedButton(boolean animated) { @@ -7018,7 +7042,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg private void didPressedButton(boolean animated) { if (buttonState == 0) { - if (MediaController.getInstance().setPlaylist(parentAdapter.audioMessages, currentMessageObject, false)) { + if (MediaController.getInstance().setPlaylist(parentAdapter.audioMessages, currentMessageObject, 0, false)) { buttonState = 1; radialProgress.setIcon(getIconForCurrentState(), false, animated); invalidate(); @@ -7445,7 +7469,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg videoView.setVisibility(INVISIBLE); videoView.loadVideo(null, null, null, null, false); HashMap args = new HashMap<>(); - args.put("Referer", "http://youtube.com"); + args.put("Referer", ApplicationLoader.applicationContext.getPackageName()); webView.loadUrl(currentBlock.url, args); } @@ -7592,7 +7616,6 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg super.onLoadResource(view, url); } - @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); @@ -7664,7 +7687,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg videoView.setVisibility(INVISIBLE); videoView.loadVideo(null, null, null, null, false); HashMap args = new HashMap<>(); - args.put("Referer", "http://youtube.com"); + args.put("Referer", ApplicationLoader.applicationContext.getPackageName()); webView.loadUrl(currentBlock.url, args); } } @@ -10474,7 +10497,6 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg DownloadController.getInstance(currentAccount).removeLoadingFileObserver(this); buttonState = -1; radialProgress.setIcon(getIconForCurrentState(), false, animated); - invalidate(); } else { DownloadController.getInstance(currentAccount).addLoadingFileObserver(fileName, null, this); float setProgress = 0; @@ -10487,8 +10509,8 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } radialProgress.setIcon(getIconForCurrentState(), true, animated); radialProgress.setProgress(setProgress, false); - invalidate(); } + invalidate(); } @Override @@ -12566,7 +12588,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg } } else if (currentAnimation != null) { imageReceiver.setImageBitmap(currentAnimation); - currentAnimation.setSecondParentView(photoContainerView); + currentAnimation.addSecondParentView(photoContainerView); } } else { if (size[0] == 0) { @@ -12977,7 +12999,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg photoContainerView.setScaleY(1.0f); } if (currentAnimation != null) { - currentAnimation.setSecondParentView(null); + currentAnimation.removeSecondParentView(photoContainerView); currentAnimation = null; centerImage.setImageBitmap((Drawable) null); } @@ -12992,7 +13014,7 @@ public class ArticleViewer implements NotificationCenter.NotificationCenterDeleg currentThumb = null; } if (currentAnimation != null) { - currentAnimation.setSecondParentView(null); + currentAnimation.removeSecondParentView(photoContainerView); currentAnimation = null; } for (int a = 0; a < 3; a++) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/BubbleActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/BubbleActivity.java new file mode 100644 index 000000000..9374ed9f2 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/BubbleActivity.java @@ -0,0 +1,345 @@ +/* + * This is the source code of Telegram for Android v. 6.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-2020. + */ + +package org.telegram.ui; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.SystemClock; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.RelativeLayout; + +import org.telegram.messenger.AccountInstance; +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.BuildVars; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.R; +import org.telegram.messenger.SharedConfig; +import org.telegram.messenger.UserConfig; +import org.telegram.ui.ActionBar.ActionBarLayout; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.DrawerLayoutContainer; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.PasscodeView; + +import java.util.ArrayList; + +public class BubbleActivity extends Activity implements ActionBarLayout.ActionBarLayoutDelegate { + + private boolean finished; + private ArrayList mainFragmentsStack = new ArrayList<>(); + + private PasscodeView passcodeView; + private ActionBarLayout actionBarLayout; + protected DrawerLayoutContainer drawerLayoutContainer; + + private Intent passcodeSaveIntent; + private boolean passcodeSaveIntentIsNew; + private int passcodeSaveIntentAccount; + private int passcodeSaveIntentState; + private boolean passcodeSaveIntentIsRestore; + + private Runnable lockRunnable; + + private long dialogId; + private int currentAccount = -1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + ApplicationLoader.postInitApplication(); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + setTheme(R.style.Theme_TMessages); + getWindow().setBackgroundDrawableResource(R.drawable.transparent); + if (SharedConfig.passcodeHash.length() > 0 && !SharedConfig.allowScreenCapture) { + try { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + } catch (Exception e) { + FileLog.e(e); + } + } + + super.onCreate(savedInstanceState); + + if (SharedConfig.passcodeHash.length() != 0 && SharedConfig.appLocked) { + SharedConfig.lastPauseTime = (int) (SystemClock.elapsedRealtime() / 1000); + } + + AndroidUtilities.fillStatusBarHeight(this); + Theme.createDialogsResources(this); + Theme.createChatResources(this, false); + + actionBarLayout = new ActionBarLayout(this); + actionBarLayout.setInBubbleMode(true); + actionBarLayout.setRemoveActionBarExtraHeight(true); + + drawerLayoutContainer = new DrawerLayoutContainer(this); + drawerLayoutContainer.setAllowOpenDrawer(false, false); + setContentView(drawerLayoutContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + RelativeLayout launchLayout = new RelativeLayout(this); + drawerLayoutContainer.addView(launchLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + launchLayout.addView(actionBarLayout, LayoutHelper.createRelative(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + drawerLayoutContainer.setParentActionBarLayout(actionBarLayout); + actionBarLayout.setDrawerLayoutContainer(drawerLayoutContainer); + actionBarLayout.init(mainFragmentsStack); + actionBarLayout.setDelegate(this); + + passcodeView = new PasscodeView(this); + drawerLayoutContainer.addView(passcodeView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.closeOtherAppActivities, this); + + actionBarLayout.removeAllFragments(); + + handleIntent(getIntent(), false, savedInstanceState != null, false, UserConfig.selectedAccount, 0); + } + + private void showPasscodeActivity() { + if (passcodeView == null) { + return; + } + SharedConfig.appLocked = true; + if (SecretMediaViewer.hasInstance() && SecretMediaViewer.getInstance().isVisible()) { + SecretMediaViewer.getInstance().closePhoto(false, false); + } else if (PhotoViewer.hasInstance() && PhotoViewer.getInstance().isVisible()) { + PhotoViewer.getInstance().closePhoto(false, true); + } else if (ArticleViewer.hasInstance() && ArticleViewer.getInstance().isVisible()) { + ArticleViewer.getInstance().close(false, true); + } + passcodeView.onShow(); + SharedConfig.isWaitingForPasscodeEnter = true; + drawerLayoutContainer.setAllowOpenDrawer(false, false); + passcodeView.setDelegate(() -> { + SharedConfig.isWaitingForPasscodeEnter = false; + if (passcodeSaveIntent != null) { + handleIntent(passcodeSaveIntent, passcodeSaveIntentIsNew, passcodeSaveIntentIsRestore, true, passcodeSaveIntentAccount, passcodeSaveIntentState); + passcodeSaveIntent = null; + } + drawerLayoutContainer.setAllowOpenDrawer(true, false); + actionBarLayout.showLastFragment(); + }); + } + + private boolean handleIntent(final Intent intent, final boolean isNew, final boolean restore, final boolean fromPassword, final int intentAccount, int state) { + if (!fromPassword && (AndroidUtilities.needShowPasscode(true) || SharedConfig.isWaitingForPasscodeEnter)) { + showPasscodeActivity(); + passcodeSaveIntent = intent; + passcodeSaveIntentIsNew = isNew; + passcodeSaveIntentIsRestore = restore; + passcodeSaveIntentAccount = intentAccount; + passcodeSaveIntentState = state; + UserConfig.getInstance(intentAccount).saveConfig(false); + return false; + } + currentAccount = intent.getIntExtra("currentAccount", UserConfig.selectedAccount); + BaseFragment chatActivity = null; + if (intent.getAction().startsWith("com.tmessages.openchat")) { + int chatId = intent.getIntExtra("chatId", 0); + int userId = intent.getIntExtra("userId", 0); + Bundle args = new Bundle(); + if (userId != 0) { + dialogId = userId; + args.putInt("user_id", userId); + } else { + dialogId = -chatId; + args.putInt("chat_id", chatId); + } + chatActivity = new ChatActivity(args); + chatActivity.setInBubbleMode(true); + chatActivity.setCurrentAccount(currentAccount); + } + if (chatActivity == null) { + finish(); + return false; + } + NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats, dialogId); + actionBarLayout.removeAllFragments(); + actionBarLayout.addFragmentToStack(chatActivity); + AccountInstance.getInstance(currentAccount).getNotificationsController().setOpenedInBubble(dialogId, true); + AccountInstance.getInstance(currentAccount).getConnectionsManager().setAppPaused(false, false); + actionBarLayout.showLastFragment(); + + return true; + } + + @Override + public boolean onPreIme() { + return false; + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent, true, false, false, UserConfig.selectedAccount, 0); + } + + private void onFinish() { + if (finished) { + return; + } + if (lockRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(lockRunnable); + lockRunnable = null; + } + finished = true; + } + + public void presentFragment(BaseFragment fragment) { + actionBarLayout.presentFragment(fragment); + } + + public boolean presentFragment(final BaseFragment fragment, final boolean removeLast, boolean forceWithoutAnimation) { + return actionBarLayout.presentFragment(fragment, removeLast, forceWithoutAnimation, true, false); + } + + @Override + protected void onPause() { + super.onPause(); + actionBarLayout.onPause(); + ApplicationLoader.externalInterfacePaused = true; + onPasscodePause(); + if (passcodeView != null) { + passcodeView.onPause(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (currentAccount != -1) { + AccountInstance.getInstance(currentAccount).getNotificationsController().setOpenedInBubble(dialogId, false); + AccountInstance.getInstance(currentAccount).getConnectionsManager().setAppPaused(false, false); + } + onFinish(); + } + + @Override + protected void onResume() { + super.onResume(); + actionBarLayout.onResume(); + ApplicationLoader.externalInterfacePaused = false; + onPasscodeResume(); + if (passcodeView.getVisibility() != View.VISIBLE) { + actionBarLayout.onResume(); + } else { + actionBarLayout.dismissDialogs(); + passcodeView.onResume(); + } + } + + private void onPasscodePause() { + if (lockRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(lockRunnable); + lockRunnable = null; + } + if (SharedConfig.passcodeHash.length() != 0) { + SharedConfig.lastPauseTime = (int) (SystemClock.elapsedRealtime() / 1000); + lockRunnable = new Runnable() { + @Override + public void run() { + if (lockRunnable == this) { + if (AndroidUtilities.needShowPasscode(true)) { + if (BuildVars.LOGS_ENABLED) { + FileLog.d("lock app"); + } + showPasscodeActivity(); + } else { + if (BuildVars.LOGS_ENABLED) { + FileLog.d("didn't pass lock check"); + } + } + lockRunnable = null; + } + } + }; + if (SharedConfig.appLocked) { + AndroidUtilities.runOnUIThread(lockRunnable, 1000); + } else if (SharedConfig.autoLockIn != 0) { + AndroidUtilities.runOnUIThread(lockRunnable, (long) SharedConfig.autoLockIn * 1000 + 1000); + } + } else { + SharedConfig.lastPauseTime = 0; + } + SharedConfig.saveConfig(); + } + + private void onPasscodeResume() { + if (lockRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(lockRunnable); + lockRunnable = null; + } + if (AndroidUtilities.needShowPasscode(true)) { + showPasscodeActivity(); + } + if (SharedConfig.lastPauseTime != 0) { + SharedConfig.lastPauseTime = 0; + SharedConfig.saveConfig(); + } + } + + @Override + public void onConfigurationChanged(android.content.res.Configuration newConfig) { + AndroidUtilities.checkDisplaySize(this, newConfig); + super.onConfigurationChanged(newConfig); + } + + @Override + public void onBackPressed() { + if (passcodeView.getVisibility() == View.VISIBLE) { + finish(); + return; + } + if (PhotoViewer.getInstance().isVisible()) { + PhotoViewer.getInstance().closePhoto(true, false); + } else if (drawerLayoutContainer.isDrawerOpened()) { + drawerLayoutContainer.closeDrawer(false); + } else { + actionBarLayout.onBackPressed(); + } + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + actionBarLayout.onLowMemory(); + } + + @Override + public boolean needPresentFragment(BaseFragment fragment, boolean removeLast, boolean forceWithoutAnimation, ActionBarLayout layout) { + return true; + } + + @Override + public boolean needAddFragmentToStack(BaseFragment fragment, ActionBarLayout layout) { + return true; + } + + @Override + public boolean needCloseLastFragment(ActionBarLayout layout) { + if (layout.fragmentsStack.size() <= 1) { + onFinish(); + finish(); + return false; + } + return true; + } + + @Override + public void onRebuildAllFragments(ActionBarLayout layout, boolean last) { + + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/CameraScanActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/CameraScanActivity.java index 79e0b69a4..e48d34992 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/CameraScanActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/CameraScanActivity.java @@ -459,7 +459,7 @@ public class CameraScanActivity extends BaseFragment implements Camera.PreviewCa return; } } - PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(10, false, false, null); + PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(PhotoAlbumPickerActivity.SELECT_TYPE_QR, false, false, null); fragment.setMaxSelectedPhotos(1, false); fragment.setAllowSearchImages(false); fragment.setDelegate(new PhotoAlbumPickerActivity.PhotoAlbumPickerActivityDelegate() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/CancelAccountDeletionActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/CancelAccountDeletionActivity.java index f736a70d5..2ac799773 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/CancelAccountDeletionActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/CancelAccountDeletionActivity.java @@ -31,6 +31,8 @@ import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.text.Editable; import android.text.InputType; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextUtils; import android.text.TextWatcher; import android.util.TypedValue; @@ -66,6 +68,7 @@ import org.telegram.ui.Components.EditTextBoldCursor; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RadialProgressView; import org.telegram.ui.Components.SlideView; +import org.telegram.ui.Components.URLSpanNoUnderline; import java.util.ArrayList; import java.util.Locale; @@ -77,9 +80,6 @@ public class CancelAccountDeletionActivity extends BaseFragment { private int currentViewNum = 0; private SlideView[] views = new SlideView[5]; private AlertDialog progressDialog; - private Dialog permissionsDialog; - private ArrayList permissionsItems = new ArrayList<>(); - private boolean checkPermissions = false; //true; private View doneButton; private String hash; private String phone; @@ -89,7 +89,7 @@ public class CancelAccountDeletionActivity extends BaseFragment { private final static int done_button = 1; - private class ProgressView extends View { + private static class ProgressView extends View { private Paint paint = new Paint(); private Paint paint2 = new Paint(); @@ -201,21 +201,8 @@ public class CancelAccountDeletionActivity extends BaseFragment { AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); } - @Override - public void onRequestPermissionsResultFragment(int requestCode, String[] permissions, int[] grantResults) { - if (requestCode == 6) { - checkPermissions = false; - if (currentViewNum == 0) { - views[currentViewNum].onNextPressed(); - } - } - } - @Override protected void onDialogDismiss(Dialog dialog) { - if (Build.VERSION.SDK_INT >= 23 && dialog == permissionsDialog && !permissionsItems.isEmpty()) { - getParentActivity().requestPermissions(permissionsItems.toArray(new String[0]), 6); - } if (dialog == errorDialog) { finishFragment(); } @@ -261,9 +248,6 @@ public class CancelAccountDeletionActivity extends BaseFragment { public void setPage(int page, boolean animated, Bundle params, boolean back) { if (page == 3 || page == 0) { - if (page == 0) { - //checkPermissions = true; - } doneButton.setVisibility(View.GONE); } else { doneButton.setVisibility(View.VISIBLE); @@ -355,36 +339,11 @@ public class CancelAccountDeletionActivity extends BaseFragment { return; } TelephonyManager tm = (TelephonyManager) ApplicationLoader.applicationContext.getSystemService(Context.TELEPHONY_SERVICE); - boolean simcardAvailable = tm.getSimState() != TelephonyManager.SIM_STATE_ABSENT && tm.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE; - boolean allowCall = true; - if (Build.VERSION.SDK_INT >= 23 && simcardAvailable) { - //allowCall = getParentActivity().checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED; - /*if (checkPermissions) { - permissionsItems.clear(); - if (!allowCall) { - permissionsItems.add(Manifest.permission.READ_PHONE_STATE); - } - if (!permissionsItems.isEmpty()) { - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - if (preferences.getBoolean("firstlogin", true) || getParentActivity().shouldShowRequestPermissionRationale(Manifest.permission.READ_PHONE_STATE)) { - preferences.edit().putBoolean("firstlogin", false).commit(); - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); - builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); - builder.setMessage(LocaleController.getString("AllowReadCall", R.string.AllowReadCall)); - permissionsDialog = showDialog(builder.create()); - } else { - getParentActivity().requestPermissions(permissionsItems.toArray(new String[permissionsItems.size()]), 6); - } - return; - } - }*/ - } final TLRPC.TL_account_sendConfirmPhoneCode req = new TLRPC.TL_account_sendConfirmPhoneCode(); req.hash = hash; req.settings = new TLRPC.TL_codeSettings(); - req.settings.allow_flashcall = false;//simcardAvailable && allowCall; + req.settings.allow_flashcall = false; req.settings.allow_app_hash = ApplicationLoader.hasPlayServices; SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); if (req.settings.allow_flashcall) { @@ -441,12 +400,10 @@ public class CancelAccountDeletionActivity extends BaseFragment { private ImageView blueImageView; private TextView timeText; private TextView problemText; - private Bundle currentParams; private ProgressView progressView; private Timer timeTimer; private Timer codeTimer; - private int openTime; private final Object timerSync = new Object(); private int time = 60000; private int codeTime = 15000; @@ -469,7 +426,9 @@ public class CancelAccountDeletionActivity extends BaseFragment { setOrientation(VERTICAL); confirmTextView = new TextView(context); + confirmTextView.setLinkTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteLinkText)); confirmTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText6)); + confirmTextView.setHighlightColor(Theme.getColor(Theme.key_windowBackgroundWhiteLinkSelection)); confirmTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); confirmTextView.setLineSpacing(AndroidUtilities.dp(2), 1.0f); @@ -601,14 +560,15 @@ public class CancelAccountDeletionActivity extends BaseFragment { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (currentType != 3 && blueImageView != null) { int innerHeight = blueImageView.getMeasuredHeight() + titleTextView.getMeasuredHeight() + confirmTextView.getMeasuredHeight() + AndroidUtilities.dp(18 + 17); + if (timeText.getVisibility() == VISIBLE) { + innerHeight += timeText.getMeasuredHeight(); + } int requiredHeight = AndroidUtilities.dp(80); - int maxHeight = AndroidUtilities.dp(291); + int maxHeight = AndroidUtilities.dp(340); if (scrollHeight - innerHeight < requiredHeight) { setMeasuredDimension(getMeasuredWidth(), innerHeight + requiredHeight); - } else if (scrollHeight > maxHeight) { - setMeasuredDimension(getMeasuredWidth(), maxHeight); } else { - setMeasuredDimension(getMeasuredWidth(), scrollHeight); + setMeasuredDimension(getMeasuredWidth(), Math.min(scrollHeight, maxHeight)); } } } @@ -697,11 +657,9 @@ public class CancelAccountDeletionActivity extends BaseFragment { NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.didReceiveCall); } - currentParams = params; phone = params.getString("phone"); phoneHash = params.getString("phoneHash"); timeout = time = params.getInt("timeout"); - openTime = (int) (System.currentTimeMillis() / 1000); nextType = params.getInt("nextType"); pattern = params.getString("pattern"); length = params.getInt("length"); @@ -811,8 +769,18 @@ public class CancelAccountDeletionActivity extends BaseFragment { } String number = PhoneFormat.getInstance().format(phone); - CharSequence str = AndroidUtilities.replaceTags(LocaleController.formatString("CancelAccountResetInfo", R.string.CancelAccountResetInfo, PhoneFormat.getInstance().format("+" + number))); - confirmTextView.setText(str); + + SpannableStringBuilder spanned = new SpannableStringBuilder(AndroidUtilities.replaceTags(LocaleController.formatString("CancelAccountResetInfo2", R.string.CancelAccountResetInfo2, PhoneFormat.getInstance().format("+" + number)))); + + int index1 = TextUtils.indexOf(spanned, '*'); + int index2 = TextUtils.lastIndexOf(spanned, '*'); + if (index1 != -1 && index2 != -1 && index1 != index2) { + confirmTextView.setMovementMethod(new AndroidUtilities.LinkMovementMethodMy()); + spanned.replace(index2, index2 + 1, ""); + spanned.replace(index1, index1 + 1, ""); + spanned.setSpan(new URLSpanNoUnderline("tg://settings/change_number"), index1, index2 - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + confirmTextView.setText(spanned); if (currentType != 3) { AndroidUtilities.showKeyboard(codeField[0]); @@ -903,58 +871,55 @@ public class CancelAccountDeletionActivity extends BaseFragment { if (timeTimer == null) { return; } - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - final double currentTime = System.currentTimeMillis(); - double diff = currentTime - lastCurrentTime; - time -= diff; - lastCurrentTime = currentTime; - if (time >= 1000) { - int minutes = time / 1000 / 60; - int seconds = time / 1000 - minutes * 60; - if (nextType == 4 || nextType == 3) { - timeText.setText(LocaleController.formatString("CallText", R.string.CallText, minutes, seconds)); - } else if (nextType == 2) { - timeText.setText(LocaleController.formatString("SmsText", R.string.SmsText, minutes, seconds)); - } - if (progressView != null) { - progressView.setProgress(1.0f - (float) time / (float) timeout); - } - } else { - if (progressView != null) { - progressView.setProgress(1.0f); - } - destroyTimer(); - if (currentType == 3) { - AndroidUtilities.setWaitingForCall(false); - NotificationCenter.getGlobalInstance().removeObserver(LoginActivitySmsView.this, NotificationCenter.didReceiveCall); + AndroidUtilities.runOnUIThread(() -> { + final double currentTime = System.currentTimeMillis(); + double diff = currentTime - lastCurrentTime; + time -= diff; + lastCurrentTime = currentTime; + if (time >= 1000) { + int minutes = time / 1000 / 60; + int seconds = time / 1000 - minutes * 60; + if (nextType == 4 || nextType == 3) { + timeText.setText(LocaleController.formatString("CallText", R.string.CallText, minutes, seconds)); + } else if (nextType == 2) { + timeText.setText(LocaleController.formatString("SmsText", R.string.SmsText, minutes, seconds)); + } + if (progressView != null) { + progressView.setProgress(1.0f - (float) time / (float) timeout); + } + } else { + if (progressView != null) { + progressView.setProgress(1.0f); + } + destroyTimer(); + if (currentType == 3) { + AndroidUtilities.setWaitingForCall(false); + NotificationCenter.getGlobalInstance().removeObserver(LoginActivitySmsView.this, NotificationCenter.didReceiveCall); + waitingForEvent = false; + destroyCodeTimer(); + resendCode(); + } else if (currentType == 2 || currentType == 4) { + if (nextType == 4 || nextType == 2) { + if (nextType == 4) { + timeText.setText(LocaleController.getString("Calling", R.string.Calling)); + } else { + timeText.setText(LocaleController.getString("SendingSms", R.string.SendingSms)); + } + createCodeTimer(); + TLRPC.TL_auth_resendCode req = new TLRPC.TL_auth_resendCode(); + req.phone_number = phone; + req.phone_code_hash = phoneHash; + ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> { + if (error != null && error.text != null) { + AndroidUtilities.runOnUIThread(() -> lastError = error.text); + } + }, ConnectionsManager.RequestFlagFailOnServerErrors); + } else if (nextType == 3) { + AndroidUtilities.setWaitingForSms(false); + NotificationCenter.getGlobalInstance().removeObserver(LoginActivitySmsView.this, NotificationCenter.didReceiveSmsCode); waitingForEvent = false; destroyCodeTimer(); resendCode(); - } else if (currentType == 2 || currentType == 4) { - if (nextType == 4 || nextType == 2) { - if (nextType == 4) { - timeText.setText(LocaleController.getString("Calling", R.string.Calling)); - } else { - timeText.setText(LocaleController.getString("SendingSms", R.string.SendingSms)); - } - createCodeTimer(); - TLRPC.TL_auth_resendCode req = new TLRPC.TL_auth_resendCode(); - req.phone_number = phone; - req.phone_code_hash = phoneHash; - ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> { - if (error != null && error.text != null) { - AndroidUtilities.runOnUIThread(() -> lastError = error.text); - } - }, ConnectionsManager.RequestFlagFailOnServerErrors); - } else if (nextType == 3) { - AndroidUtilities.setWaitingForSms(false); - NotificationCenter.getGlobalInstance().removeObserver(LoginActivitySmsView.this, NotificationCenter.didReceiveSmsCode); - waitingForEvent = false; - destroyCodeTimer(); - resendCode(); - } } } } @@ -1122,6 +1087,7 @@ public class CancelAccountDeletionActivity extends BaseFragment { arrayList.add(new ThemeDescription(phoneView.progressBar, ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_progressCircle)); arrayList.add(new ThemeDescription(smsView1.confirmTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteGrayText6)); + arrayList.add(new ThemeDescription(smsView1.confirmTextView, ThemeDescription.FLAG_LINKCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteLinkText)); arrayList.add(new ThemeDescription(smsView1.titleTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); if (smsView1.codeField != null) { for (int a = 0; a < smsView1.codeField.length; a++) { 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 71155d233..401d58a1f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java @@ -22,9 +22,12 @@ import android.text.style.URLSpan; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.TextView; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.Emoji; import org.telegram.messenger.FileLog; @@ -65,6 +68,7 @@ public class AboutLinkCell extends FrameLayout { valueTextView.setMaxLines(1); valueTextView.setSingleLine(true); valueTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); + valueTextView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(valueTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.BOTTOM, 23, 0, 23, 10)); setWillNotDraw(false); @@ -86,7 +90,7 @@ public class AboutLinkCell extends FrameLayout { } public void setTextAndValue(String text, String value, boolean parseLinks) { - if (TextUtils.isEmpty(text) || text != null && text.equals(oldText)) { + if (TextUtils.isEmpty(text) || TextUtils.equals(text, oldText)) { return; } oldText = text; @@ -215,4 +219,18 @@ public class AboutLinkCell extends FrameLayout { } canvas.restore(); } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if (textLayout != null) { + final CharSequence text = textLayout.getText(); + final CharSequence valueText = valueTextView.getText(); + if (TextUtils.isEmpty(valueText)) { + info.setText(text); + } else { + info.setText(valueText + ": " + text); + } + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/BrightnessControlCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/BrightnessControlCell.java index d44e93449..7f72abeae 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/BrightnessControlCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/BrightnessControlCell.java @@ -11,8 +11,10 @@ package org.telegram.ui.Cells; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.os.Bundle; import android.view.Gravity; import android.view.MotionEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.ImageView; @@ -35,7 +37,7 @@ public class BrightnessControlCell extends FrameLayout { leftImageView.setImageResource(R.drawable.brightness_low); addView(leftImageView, LayoutHelper.createFrame(24, 24, Gravity.LEFT | Gravity.TOP, 17, 12, 0, 0)); - seekBarView = new SeekBarView(context) { + seekBarView = new SeekBarView(context, /* inPercents = */ true) { @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { @@ -53,9 +55,14 @@ public class BrightnessControlCell extends FrameLayout { @Override public void onSeekBarPressed(boolean pressed) { + } + @Override + public CharSequence getContentDescription() { + return " "; } }); + seekBarView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(seekBarView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 38, Gravity.TOP | Gravity.LEFT, 54, 5, 54, 0)); rightImageView = new ImageView(context); @@ -82,4 +89,15 @@ public class BrightnessControlCell extends FrameLayout { public void setProgress(float value) { seekBarView.setProgress(value); } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + seekBarView.getSeekBarAccessibilityDelegate().onInitializeAccessibilityNodeInfoInternal(this, info); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + return super.performAccessibilityAction(action, arguments) || seekBarView.getSeekBarAccessibilityDelegate().performAccessibilityActionInternal(this, action, arguments); + } } 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 b2e8d1147..b752df05d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java @@ -24,6 +24,8 @@ import android.view.SoundEffectConstants; import android.view.accessibility.AccessibilityNodeInfo; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.DownloadController; +import org.telegram.messenger.ImageLoader; import org.telegram.messenger.ImageLocation; import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.LocaleController; @@ -31,6 +33,7 @@ import org.telegram.messenger.MessageObject; import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; +import org.telegram.messenger.SharedConfig; import org.telegram.messenger.browser.Browser; import org.telegram.tgnet.TLRPC; import org.telegram.messenger.UserConfig; @@ -38,7 +41,7 @@ import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.PhotoViewer; import org.telegram.ui.Components.AvatarDrawable; -public class ChatActionCell extends BaseCell { +public class ChatActionCell extends BaseCell implements DownloadController.FileDownloadProgressListener { public interface ChatActionCellDelegate { default void didClickImage(ChatActionCell cell) { @@ -57,6 +60,8 @@ public class ChatActionCell extends BaseCell { } } + private int TAG; + private URLSpan pressedLink; private int currentAccount = UserConfig.selectedAccount; private ImageReceiver imageReceiver; @@ -70,6 +75,8 @@ public class ChatActionCell extends BaseCell { private int previousWidth; private boolean imagePressed; + private ImageLocation currentVideoLocation; + private float lastTouchX; private float lastTouchY; @@ -91,8 +98,9 @@ public class ChatActionCell extends BaseCell { public ChatActionCell(Context context) { super(context); imageReceiver = new ImageReceiver(this); - imageReceiver.setRoundRadius(AndroidUtilities.dp(32)); + imageReceiver.setRoundRadius(AndroidUtilities.roundMessageSize / 2); avatarDrawable = new AvatarDrawable(); + TAG = DownloadController.getInstance(currentAccount).generateObserverTag(); } public void setDelegate(ChatActionCellDelegate delegate) { @@ -118,6 +126,10 @@ public class ChatActionCell extends BaseCell { } customDate = date; customText = newText; + updateTextInternal(inLayout); + } + + private void updateTextInternal(boolean inLayout) { if (getMeasuredWidth() != 0) { createLayout(customText, getMeasuredWidth()); invalidate(); @@ -133,6 +145,13 @@ public class ChatActionCell extends BaseCell { } } + public void setCustomText(String text) { + customText = text; + if (customText != null) { + updateTextInternal(false); + } + } + public void setOverrideColor(String background, String text) { overrideBackground = background; overrideText = text; @@ -144,6 +163,7 @@ public class ChatActionCell extends BaseCell { } currentMessageObject = messageObject; hasReplyMessage = messageObject.replyMessageObject != null; + DownloadController.getInstance(currentAccount).removeLoadingFileObserver(this); previousWidth = 0; if (currentMessageObject.type == 11) { int id = 0; @@ -163,9 +183,32 @@ public class ChatActionCell extends BaseCell { if (currentMessageObject.messageOwner.action instanceof TLRPC.TL_messageActionUserUpdatedPhoto) { imageReceiver.setImage(null, null, avatarDrawable, null, currentMessageObject, 0); } else { - TLRPC.PhotoSize photo = FileLoader.getClosestPhotoSizeWithSize(currentMessageObject.photoThumbs, AndroidUtilities.dp(64)); - if (photo != null) { - imageReceiver.setImage(ImageLocation.getForObject(photo, currentMessageObject.photoThumbsObject), "50_50", avatarDrawable, null, currentMessageObject, 0); + TLRPC.PhotoSize strippedPhotoSize = null; + for (int a = 0, N = currentMessageObject.photoThumbs.size(); a < N; a++) { + TLRPC.PhotoSize photoSize = currentMessageObject.photoThumbs.get(a); + if (photoSize instanceof TLRPC.TL_photoStrippedSize) { + strippedPhotoSize = photoSize; + break; + } + } + TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(currentMessageObject.photoThumbs, 640); + if (photoSize != null) { + TLRPC.Photo photo = messageObject.messageOwner.action.photo; + TLRPC.VideoSize videoSize = null; + if (!photo.video_sizes.isEmpty() && SharedConfig.autoplayGifs) { + videoSize = photo.video_sizes.get(0); + if (!messageObject.mediaExists && !DownloadController.getInstance(currentAccount).canDownloadMedia(DownloadController.AUTODOWNLOAD_TYPE_VIDEO, videoSize.size)) { + currentVideoLocation = ImageLocation.getForPhoto(videoSize, photo); + String fileName = FileLoader.getAttachFileName(videoSize); + DownloadController.getInstance(currentAccount).addLoadingFileObserver(fileName, currentMessageObject, this); + videoSize = null; + } + } + if (videoSize != null) { + imageReceiver.setImage(ImageLocation.getForPhoto(videoSize, photo), ImageLoader.AUTOPLAY_FILTER, ImageLocation.getForObject(strippedPhotoSize, currentMessageObject.photoThumbsObject), "50_50_b", avatarDrawable, 0, null, currentMessageObject, 1); + } else { + imageReceiver.setImage(ImageLocation.getForObject(photoSize, currentMessageObject.photoThumbsObject), "150_150", ImageLocation.getForObject(strippedPhotoSize, currentMessageObject.photoThumbsObject), "50_50_b", avatarDrawable, 0, null, currentMessageObject, 1); + } } else { imageReceiver.setImageBitmap(avatarDrawable); } @@ -200,9 +243,17 @@ public class ChatActionCell extends BaseCell { @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + DownloadController.getInstance(currentAccount).removeLoadingFileObserver(this); + imageReceiver.onDetachedFromWindow(); wasLayout = false; } + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + imageReceiver.onAttachedToWindow(); + } + @Override public boolean onTouchEvent(MotionEvent event) { if (currentMessageObject == null) { @@ -342,6 +393,7 @@ public class ChatActionCell extends BaseCell { textXLeft = (width - textLayout.getWidth()) / 2; } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (currentMessageObject == null && customText == null) { @@ -354,7 +406,7 @@ public class ChatActionCell extends BaseCell { previousWidth = width; buildLayout(); } - setMeasuredDimension(width, textHeight + AndroidUtilities.dp(14 + (currentMessageObject != null && currentMessageObject.type == 11 ? 70 : 0))); + setMeasuredDimension(width, textHeight + (currentMessageObject != null && currentMessageObject.type == 11 ? AndroidUtilities.roundMessageSize + AndroidUtilities.dp(10) : 0) + AndroidUtilities.dp(14)); } private void buildLayout() { @@ -376,7 +428,7 @@ public class ChatActionCell extends BaseCell { } createLayout(text, previousWidth); if (currentMessageObject != null && currentMessageObject.type == 11) { - imageReceiver.setImageCoords((previousWidth - AndroidUtilities.dp(64)) / 2, textHeight + AndroidUtilities.dp(15), AndroidUtilities.dp(64), AndroidUtilities.dp(64)); + imageReceiver.setImageCoords((previousWidth - AndroidUtilities.roundMessageSize) / 2, textHeight + AndroidUtilities.dp(19), AndroidUtilities.roundMessageSize, AndroidUtilities.roundMessageSize); } } @@ -639,6 +691,42 @@ public class ChatActionCell extends BaseCell { } } + @Override + public void onFailedDownload(String fileName, boolean canceled) { + + } + + @Override + public void onSuccessDownload(String fileName) { + if (currentMessageObject != null && currentMessageObject.type == 11) { + TLRPC.PhotoSize strippedPhotoSize = null; + for (int a = 0, N = currentMessageObject.photoThumbs.size(); a < N; a++) { + TLRPC.PhotoSize photoSize = currentMessageObject.photoThumbs.get(a); + if (photoSize instanceof TLRPC.TL_photoStrippedSize) { + strippedPhotoSize = photoSize; + break; + } + } + imageReceiver.setImage(currentVideoLocation, ImageLoader.AUTOPLAY_FILTER, ImageLocation.getForObject(strippedPhotoSize, currentMessageObject.photoThumbsObject), "50_50_b", avatarDrawable, 0, null, currentMessageObject, 1); + DownloadController.getInstance(currentAccount).removeLoadingFileObserver(this); + } + } + + @Override + public void onProgressDownload(String fileName, long downloadSize, long totalSize) { + + } + + @Override + public void onProgressUpload(String fileName, long downloadSize, long totalSize, boolean isEncrypted) { + + } + + @Override + public int getObserverTag() { + return TAG; + } + @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatListCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatListCell.java index e6f5b8dc1..1a7b6a83d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatListCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatListCell.java @@ -7,6 +7,7 @@ import android.graphics.Paint; import android.graphics.RectF; import android.text.TextPaint; import android.view.Gravity; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -32,6 +33,7 @@ public class ChatListCell extends LinearLayout { setWillNotDraw(false); isThreeLines = threeLines; + setContentDescription(threeLines ? LocaleController.getString("ChatListExpanded", R.string.ChatListExpanded) : LocaleController.getString("ChatListDefault", R.string.ChatListDefault)); textPaint.setTextSize(AndroidUtilities.dp(13)); @@ -87,6 +89,14 @@ public class ChatListCell extends LinearLayout { } } } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(RadioButton.class.getName()); + info.setChecked(button.isChecked()); + info.setCheckable(true); + } } private ListView[] listView = new ListView[2]; 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 5f7996c34..9bb51729c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java @@ -100,6 +100,7 @@ import org.telegram.ui.Components.BackgroundGradientDrawable; import org.telegram.ui.Components.CheckBoxBase; import org.telegram.ui.Components.CubicBezierInterpolator; import org.telegram.ui.Components.EmptyStubSpan; +import org.telegram.ui.Components.FloatSeekBarAccessibilityDelegate; import org.telegram.ui.Components.LinkPath; import org.telegram.ui.Components.MediaActionDrawable; import org.telegram.ui.Components.MessageBackgroundDrawable; @@ -108,6 +109,7 @@ import org.telegram.ui.Components.RLottieDrawable; import org.telegram.ui.Components.RadialProgress2; import org.telegram.ui.Components.RoundVideoPlayingDrawable; import org.telegram.ui.Components.SeekBar; +import org.telegram.ui.Components.SeekBarAccessibilityDelegate; import org.telegram.ui.Components.SeekBarWaveform; import org.telegram.ui.Components.StaticLayoutEx; import org.telegram.ui.Components.TextStyleSpan; @@ -249,6 +251,22 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private int angle; private float progressAlpha; private long lastUpdateTime; + private TransitionParams transitionParams = new TransitionParams(); + + private class TransitionParams { + int transitionType; + int fromX; + int fromY; + int fromWidth; + int fromHeight; + + void saveState() { + fromX = x; + fromY = y; + fromWidth = width; + fromHeight = height; + } + } } public static class PollButton { @@ -300,6 +318,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private boolean attachedToWindow; + private boolean isUpdating; + private RadialProgress2 radialProgress; private RadialProgress2 videoRadialProgress; private boolean drawRadialCheckBackground; @@ -456,6 +476,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private boolean useSeekBarWaweform; private SeekBar seekBar; private SeekBarWaveform seekBarWaveform; + private SeekBarAccessibilityDelegate seekBarAccessibilityDelegate; private int seekBarX; private int seekBarY; @@ -663,6 +684,15 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private final TransitionParams transitionParams = new TransitionParams(); private boolean edited; + private Runnable diceFinishCallback = new Runnable() { + @Override + public void run() { + if (delegate != null) { + delegate.onDiceFinished(); + } + } + }; + private Runnable invalidateRunnable = new Runnable() { @Override public void run() { @@ -710,6 +740,39 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate seekBarWaveform = new SeekBarWaveform(context); seekBarWaveform.setDelegate(this); seekBarWaveform.setParentView(this); + seekBarAccessibilityDelegate = new FloatSeekBarAccessibilityDelegate() { + @Override + public float getProgress() { + if (currentMessageObject.isMusic()) { + return seekBar.getProgress(); + } else if (currentMessageObject.isVoice()) { + if (useSeekBarWaweform) { + return seekBarWaveform.getProgress(); + } else { + return seekBar.getProgress(); + } + } else { + return 0f; + } + } + + @Override + public void setProgress(float progress) { + if (currentMessageObject.isMusic()) { + seekBar.setProgress(progress); + } else if (currentMessageObject.isVoice()) { + if (useSeekBarWaweform) { + seekBarWaveform.setProgress(progress); + } else { + seekBar.setProgress(progress); + } + } else { + return; + } + onSeekBarDrag(progress); + invalidate(); + } + }; roundVideoPlayingDrawable = new RoundVideoPlayingDrawable(this); } @@ -1541,7 +1604,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (useSeekBarWaweform) { result = seekBarWaveform.onTouch(event.getAction(), event.getX() - seekBarX - AndroidUtilities.dp(13), event.getY() - seekBarY); } else { - result = seekBar.onTouch(event.getAction(), event.getX() - seekBarX, event.getY() - seekBarY); + if (MediaController.getInstance().isPlayingMessage(currentMessageObject)) { + result = seekBar.onTouch(event.getAction(), event.getX() - seekBarX, event.getY() - seekBarY); + } else { + result = false; + } } if (result) { if (!useSeekBarWaweform && event.getAction() == MotionEvent.ACTION_DOWN) { @@ -2536,7 +2603,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate boolean groupChanged = groupedMessages != currentMessagesGroup; boolean pollChanged = false; if (!messageChanged && messageObject.isDice()) { - setCurrentDiceValue(false); + setCurrentDiceValue(isUpdating); } if (!messageChanged && messageObject.isPoll()) { ArrayList newResults = null; @@ -3068,7 +3135,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (lineLeft != 0) { width = titleLayout.getWidth() - lineLeft; } else { - width = (int) Math.ceil(titleLayout.getLineWidth(a)); + int max = linkPreviewMaxWidth; + if (a < restLines || lineLeft != 0 && isSmallImage) { + max -= AndroidUtilities.dp(48 + 4); + } + width = (int) Math.min(max, Math.ceil(titleLayout.getLineWidth(a))); } if (a < restLines || lineLeft != 0 && isSmallImage) { width += AndroidUtilities.dp(48 + 4); @@ -3544,7 +3615,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate String filter = currentPhotoObject instanceof TLRPC.TL_photoStrippedSize || "s".equals(currentPhotoObject.type) ? currentPhotoFilterThumb : currentPhotoFilter; if (messageObject.mediaExists || autoDownload) { autoPlayingMedia = true; - TLRPC.TL_videoSize videoSize = MessageObject.getDocumentVideoThumb(document); + TLRPC.VideoSize videoSize = MessageObject.getDocumentVideoThumb(document); if (!messageObject.mediaExists && videoSize != null && (currentPhotoObject == null || currentPhotoObjectThumb == null)) { photoImage.setImage(ImageLocation.getForDocument(document), document.size < 1024 * 32 ? null : ImageLoader.AUTOPLAY_FILTER, ImageLocation.getForDocument(videoSize, documentAttach), null, ImageLocation.getForDocument(currentPhotoObject != null ? currentPhotoObject : currentPhotoObjectThumb, documentAttach), currentPhotoObject != null ? filter : currentPhotoFilterThumb, null, document.size, null, messageObject, 0); } else { @@ -4367,9 +4438,22 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate int w = (int) (photoWidth / AndroidUtilities.density); int h = (int) (photoHeight / AndroidUtilities.density); boolean shouldRepeatSticker = delegate != null && delegate.shouldRepeatSticker(messageObject); + currentPhotoObjectThumb = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 40); + photoParentObject = messageObject.photoThumbsObject; if (messageObject.isDice()) { filter = String.format(Locale.US, "%d_%d_dice_%s_%s", w, h, messageObject.getDiceEmoji(), messageObject.toString()); photoImage.setAutoRepeat(2); + TLRPC.TL_messages_stickerSet stickerSet = MediaDataController.getInstance(currentAccount).getDiceStickerSetByEmoji(currentMessageObject.getDiceEmoji()); + if (stickerSet != null) { + if (stickerSet.documents.size() > 0) { + int value = currentMessageObject.getDiceValue(); + if (value <= 0) { + TLRPC.Document document = stickerSet.documents.get(0); + currentPhotoObjectThumb = FileLoader.getClosestPhotoSizeWithSize(document.thumbs, 40); + photoParentObject = document; + } + } + } } else if (messageObject.isAnimatedEmoji()) { filter = String.format(Locale.US, "%d_%d_nr_%s" + messageObject.emojiAnimatedStickerColor, w, h, messageObject.toString()); photoImage.setAutoRepeat(shouldRepeatSticker ? 2 : 3); @@ -4385,8 +4469,6 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate availableTimeWidth = photoWidth - AndroidUtilities.dp(14); backgroundWidth = photoWidth + AndroidUtilities.dp(12); - currentPhotoObjectThumb = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 40); - photoParentObject = messageObject.photoThumbsObject; photoImage.setRoundRadius(0); canChangeRadius = false; if (messageObject.attachPathExists) { @@ -4872,7 +4954,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (MessageObject.isGifDocument(document) || messageObject.type == MessageObject.TYPE_ROUND_VIDEO) { autoDownload = DownloadController.getInstance(currentAccount).canDownloadMedia(currentMessageObject); } - TLRPC.TL_videoSize videoSize = MessageObject.getDocumentVideoThumb(document); + TLRPC.VideoSize videoSize = MessageObject.getDocumentVideoThumb(document); if (((MessageObject.isGifDocument(document) && messageObject.videoEditedInfo == null) || (!messageObject.isSending() && !messageObject.isEditing())) && (localFile != 0 || FileLoader.getInstance(currentAccount).isLoadingFile(fileName) || autoDownload)) { if (localFile != 1 && !messageObject.needDrawBluredPreview() && (localFile != 0 || messageObject.canStreamVideo() && autoDownload)) { autoPlayingMedia = true; @@ -4953,22 +5035,12 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } int y; - if (currentPosition != null) { - if (namesOffset > 0) { - y = AndroidUtilities.dp(7); - totalHeight -= AndroidUtilities.dp(2); - } else { - y = AndroidUtilities.dp(5); - totalHeight -= AndroidUtilities.dp(4); - } + if (namesOffset > 0) { + y = AndroidUtilities.dp(7); + totalHeight -= AndroidUtilities.dp(2); } else { - if (namesOffset > 0) { - y = AndroidUtilities.dp(7); - totalHeight -= AndroidUtilities.dp(2); - } else { - y = AndroidUtilities.dp(5); - totalHeight -= AndroidUtilities.dp(4); - } + y = AndroidUtilities.dp(5); + totalHeight -= AndroidUtilities.dp(4); } photoImage.setImageCoords(0, y + namesOffset + additionalTop, photoWidth, photoHeight); invalidate(); @@ -5561,7 +5633,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate measureTime(messageObject); int minSize = AndroidUtilities.dp(40 + 14 + 20 + 90 + 10) + timeWidth; if (!hasLinkPreview) { - backgroundWidth = Math.min(maxWidth, minSize + duration * AndroidUtilities.dp(10)); + String timeString = AndroidUtilities.formatLongDuration(duration); + int w = (int) Math.ceil(Theme.chat_audioTimePaint.measureText(timeString)); + backgroundWidth = Math.min(maxWidth, minSize + w); } seekBarWaveform.setMessageObject(messageObject); return 0; @@ -5818,6 +5892,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } + public void setIsUpdating(boolean value) { + isUpdating = true; + } + public void setMessageObject(MessageObject messageObject, MessageObject.GroupedMessages groupedMessages, boolean bottomNear, boolean topNear) { if (attachedToWindow) { setMessageContent(messageObject, groupedMessages, bottomNear, topNear); @@ -6315,6 +6393,19 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate setVisiblePart(scrollRect.top, scrollRect.bottom - scrollRect.top, parentHeight); needNewVisiblePart = false; } + + int buttonX = this.buttonX; + int buttonY = this.buttonY; + if (transitionParams.animateButton) { + buttonX = (int) (transitionParams.animateFromButtonX * (1f - transitionParams.animateChangeProgress) + this.buttonX * (transitionParams.animateChangeProgress)); + buttonY = (int) (transitionParams.animateFromButtonY * (1f - transitionParams.animateChangeProgress) + this.buttonY * (transitionParams.animateChangeProgress)); + radialProgress.setProgressRect(buttonX, buttonY, buttonX + AndroidUtilities.dp(44), buttonY + AndroidUtilities.dp(44)); + } + if (transitionParams.animateBackgroundBoundsInner && documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO) { + int backgroundWidth = this.backgroundWidth - transitionParams.deltaLeft + transitionParams.deltaRight; + seekBarWaveform.setSize(backgroundWidth - AndroidUtilities.dp(92 + (hasLinkPreview ? 10 : 0)), AndroidUtilities.dp(30)); + seekBar.setSize(backgroundWidth - AndroidUtilities.dp(72 + (hasLinkPreview ? 10 : 0)), AndroidUtilities.dp(30)); + } forceNotDrawTime = currentMessagesGroup != null; photoImage.setPressed((isHighlightedAnimated || isHighlighted) && currentPosition != null ? 2 : 0); photoImage.setVisible(!PhotoViewer.isShowingImage(currentMessageObject) && !SecretMediaViewer.getInstance().isShowingImage(currentMessageObject), false); @@ -6459,8 +6550,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (drawPhotoImage && drawInstantView) { if (drawImageButton) { int size = AndroidUtilities.dp(48); - buttonX = (int) (photoImage.getImageX() + (photoImage.getImageWidth() - size) / 2.0f); - buttonY = (int) (photoImage.getImageY() + (photoImage.getImageHeight() - size) / 2.0f); + buttonX = this.buttonX = (int) (photoImage.getImageX() + (photoImage.getImageWidth() - size) / 2.0f); + buttonY = this.buttonY = (int) (photoImage.getImageY() + (photoImage.getImageHeight() - size) / 2.0f); radialProgress.setProgressRect(buttonX, buttonY, buttonX + size, buttonY + size); } imageDrawn = photoImage.draw(canvas); @@ -6535,8 +6626,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate photoImage.setImageCoords(linkX + (hasInvoicePreview ? -AndroidUtilities.dp(6.3f) : AndroidUtilities.dp(10)), linkPreviewY, photoImage.getImageWidth(), photoImage.getImageHeight()); if (drawImageButton) { int size = AndroidUtilities.dp(48); - buttonX = (int) (photoImage.getImageX() + (photoImage.getImageWidth() - size) / 2.0f); - buttonY = (int) (photoImage.getImageY() + (photoImage.getImageHeight() - size) / 2.0f); + buttonX = this.buttonX = (int) (photoImage.getImageX() + (photoImage.getImageWidth() - size) / 2.0f); + buttonY = this.buttonY = (int) (photoImage.getImageY() + (photoImage.getImageHeight() - size) / 2.0f); radialProgress.setProgressRect(buttonX, buttonY, buttonX + size, buttonY + size); } } @@ -6845,6 +6936,13 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate radialProgress.setBackgroundDrawable(isDrawSelectionBackground() ? currentBackgroundSelectedDrawable : currentBackgroundDrawable); radialProgress.draw(canvas); + int seekBarX = this.seekBarX; + int timeAudioX = this.timeAudioX; + if (transitionParams.animateButton) { + int offset = this.buttonX - (int) (transitionParams.animateFromButtonX * (1f - transitionParams.animateChangeProgress) + this.buttonX * (transitionParams.animateChangeProgress)); + seekBarX -= offset; + timeAudioX -= offset; + } canvas.save(); if (useSeekBarWaweform) { canvas.translate(seekBarX + AndroidUtilities.dp(13), seekBarY); @@ -7034,69 +7132,86 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate Theme.chat_redLocationIcon.setBounds(x, y, x + w, y + h); Theme.chat_redLocationIcon.draw(canvas); } + transitionParams.recordDrawingState(); + } - if (!botButtons.isEmpty()) { - int addX; - if (currentMessageObject.isOutOwner()) { - addX = getMeasuredWidth() - widthForButtons - AndroidUtilities.dp(10); - } else { - addX = backgroundDrawableLeft + AndroidUtilities.dp(mediaBackground || drawPinnedBottom ? 1 : 7); + private void drawBotButtons(Canvas canvas, ArrayList botButtons, float alpha) { + int addX; + if (currentMessageObject.isOutOwner()) { + addX = getMeasuredWidth() - widthForButtons - AndroidUtilities.dp(10); + } else { + addX = backgroundDrawableLeft + AndroidUtilities.dp(mediaBackground || drawPinnedBottom ? 1 : 7); + } + float top = layoutHeight - AndroidUtilities.dp(2) + transitionParams.deltaBottom; + float height = 0; + for (int a = 0; a < botButtons.size(); a++) { + BotButton button = botButtons.get(a); + int bottom = button.y + button.height; + if (bottom > height) { + height = bottom; } - for (int a = 0; a < botButtons.size(); a++) { - BotButton button = botButtons.get(a); - int y = button.y + layoutHeight - AndroidUtilities.dp(2); - Theme.chat_systemDrawable.setColorFilter(a == pressedBotButton ? Theme.colorPressedFilter : Theme.colorFilter); - Theme.chat_systemDrawable.setBounds(button.x + addX, y, button.x + addX + button.width, y + button.height); - Theme.chat_systemDrawable.draw(canvas); - canvas.save(); - canvas.translate(button.x + addX + AndroidUtilities.dp(5), y + (AndroidUtilities.dp(44) - button.title.getLineBottom(button.title.getLineCount() - 1)) / 2); - button.title.draw(canvas); - canvas.restore(); - if (button.button instanceof TLRPC.TL_keyboardButtonUrl) { - int x = button.x + button.width - AndroidUtilities.dp(3) - Theme.chat_botLinkDrawalbe.getIntrinsicWidth() + addX; - setDrawableBounds(Theme.chat_botLinkDrawalbe, x, y + AndroidUtilities.dp(3)); - Theme.chat_botLinkDrawalbe.draw(canvas); - } else if (button.button instanceof TLRPC.TL_keyboardButtonSwitchInline) { - int x = button.x + button.width - AndroidUtilities.dp(3) - Theme.chat_botInlineDrawable.getIntrinsicWidth() + addX; - setDrawableBounds(Theme.chat_botInlineDrawable, x, y + AndroidUtilities.dp(3)); - Theme.chat_botInlineDrawable.draw(canvas); - } else if (button.button instanceof TLRPC.TL_keyboardButtonCallback || button.button instanceof TLRPC.TL_keyboardButtonRequestGeoLocation || button.button instanceof TLRPC.TL_keyboardButtonGame || button.button instanceof TLRPC.TL_keyboardButtonBuy || button.button instanceof TLRPC.TL_keyboardButtonUrlAuth) { - boolean drawProgress = (button.button instanceof TLRPC.TL_keyboardButtonCallback || button.button instanceof TLRPC.TL_keyboardButtonGame || button.button instanceof TLRPC.TL_keyboardButtonBuy || button.button instanceof TLRPC.TL_keyboardButtonUrlAuth) && SendMessagesHelper.getInstance(currentAccount).isSendingCallback(currentMessageObject, button.button) || - button.button instanceof TLRPC.TL_keyboardButtonRequestGeoLocation && SendMessagesHelper.getInstance(currentAccount).isSendingCurrentLocation(currentMessageObject, button.button); - if (drawProgress || !drawProgress && button.progressAlpha != 0) { - Theme.chat_botProgressPaint.setAlpha(Math.min(255, (int) (button.progressAlpha * 255))); - int x = button.x + button.width - AndroidUtilities.dp(9 + 3) + addX; - rect.set(x, y + AndroidUtilities.dp(4), x + AndroidUtilities.dp(8), y + AndroidUtilities.dp(8 + 4)); - canvas.drawArc(rect, button.angle, 220, false, Theme.chat_botProgressPaint); - invalidate(); - long newTime = System.currentTimeMillis(); - if (Math.abs(button.lastUpdateTime - System.currentTimeMillis()) < 1000) { - long delta = (newTime - button.lastUpdateTime); - float dt = 360 * delta / 2000.0f; - button.angle += dt; - button.angle -= 360 * (button.angle / 360); - if (drawProgress) { - if (button.progressAlpha < 1.0f) { - button.progressAlpha += delta / 200.0f; - if (button.progressAlpha > 1.0f) { - button.progressAlpha = 1.0f; - } + } + rect.set(0, top, getMeasuredWidth(), top + height); + if (alpha != 1f) { + canvas.saveLayerAlpha(rect, (int) (255 * alpha), Canvas.ALL_SAVE_FLAG); + } else { + canvas.save(); + } + + for (int a = 0; a < botButtons.size(); a++) { + BotButton button = botButtons.get(a); + int y = button.y + layoutHeight - AndroidUtilities.dp(2) + transitionParams.deltaBottom; + Theme.chat_systemDrawable.setColorFilter(a == pressedBotButton ? Theme.colorPressedFilter : Theme.colorFilter); + Theme.chat_systemDrawable.setBounds(button.x + addX, y, button.x + addX + button.width, y + button.height); + Theme.chat_systemDrawable.draw(canvas); + canvas.save(); + canvas.translate(button.x + addX + AndroidUtilities.dp(5), y + (AndroidUtilities.dp(44) - button.title.getLineBottom(button.title.getLineCount() - 1)) / 2); + button.title.draw(canvas); + canvas.restore(); + if (button.button instanceof TLRPC.TL_keyboardButtonUrl) { + int x = button.x + button.width - AndroidUtilities.dp(3) - Theme.chat_botLinkDrawalbe.getIntrinsicWidth() + addX; + setDrawableBounds(Theme.chat_botLinkDrawalbe, x, y + AndroidUtilities.dp(3)); + Theme.chat_botLinkDrawalbe.draw(canvas); + } else if (button.button instanceof TLRPC.TL_keyboardButtonSwitchInline) { + int x = button.x + button.width - AndroidUtilities.dp(3) - Theme.chat_botInlineDrawable.getIntrinsicWidth() + addX; + setDrawableBounds(Theme.chat_botInlineDrawable, x, y + AndroidUtilities.dp(3)); + Theme.chat_botInlineDrawable.draw(canvas); + } else if (button.button instanceof TLRPC.TL_keyboardButtonCallback || button.button instanceof TLRPC.TL_keyboardButtonRequestGeoLocation || button.button instanceof TLRPC.TL_keyboardButtonGame || button.button instanceof TLRPC.TL_keyboardButtonBuy || button.button instanceof TLRPC.TL_keyboardButtonUrlAuth) { + boolean drawProgress = (button.button instanceof TLRPC.TL_keyboardButtonCallback || button.button instanceof TLRPC.TL_keyboardButtonGame || button.button instanceof TLRPC.TL_keyboardButtonBuy || button.button instanceof TLRPC.TL_keyboardButtonUrlAuth) && SendMessagesHelper.getInstance(currentAccount).isSendingCallback(currentMessageObject, button.button) || + button.button instanceof TLRPC.TL_keyboardButtonRequestGeoLocation && SendMessagesHelper.getInstance(currentAccount).isSendingCurrentLocation(currentMessageObject, button.button); + if (drawProgress || !drawProgress && button.progressAlpha != 0) { + Theme.chat_botProgressPaint.setAlpha(Math.min(255, (int) (button.progressAlpha * 255))); + int x = button.x + button.width - AndroidUtilities.dp(9 + 3) + addX; + rect.set(x, y + AndroidUtilities.dp(4), x + AndroidUtilities.dp(8), y + AndroidUtilities.dp(8 + 4)); + canvas.drawArc(rect, button.angle, 220, false, Theme.chat_botProgressPaint); + invalidate(); + long newTime = System.currentTimeMillis(); + if (Math.abs(button.lastUpdateTime - System.currentTimeMillis()) < 1000) { + long delta = (newTime - button.lastUpdateTime); + float dt = 360 * delta / 2000.0f; + button.angle += dt; + button.angle -= 360 * (button.angle / 360); + if (drawProgress) { + if (button.progressAlpha < 1.0f) { + button.progressAlpha += delta / 200.0f; + if (button.progressAlpha > 1.0f) { + button.progressAlpha = 1.0f; } - } else { - if (button.progressAlpha > 0.0f) { - button.progressAlpha -= delta / 200.0f; - if (button.progressAlpha < 0.0f) { - button.progressAlpha = 0.0f; - } + } + } else { + if (button.progressAlpha > 0.0f) { + button.progressAlpha -= delta / 200.0f; + if (button.progressAlpha < 0.0f) { + button.progressAlpha = 0.0f; } } } - button.lastUpdateTime = newTime; } + button.lastUpdateTime = newTime; } } } - transitionParams.recordDrawingState(); + canvas.restore(); } private void drawMessageText(Canvas canvas, ArrayList textLayoutBlocks, boolean origin, float alpha) { @@ -7450,7 +7565,6 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate DownloadController.getInstance(currentAccount).addLoadingFileObserver(fileName, currentMessageObject, this); if (!FileLoader.getInstance(currentAccount).isLoadingFile(fileName)) { buttonState = 2; - radialProgress.setIcon(getIconForCurrentState(), ifSame, animated); } else { buttonState = 4; long[] progress = ImageLoader.getInstance().getFileProgressSizes(fileName); @@ -7461,8 +7575,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate createLoadingProgressLayout(documentAttach); radialProgress.setProgress(0, animated); } - radialProgress.setIcon(getIconForCurrentState(), ifSame, animated); } + radialProgress.setIcon(getIconForCurrentState(), ifSame, animated); } } updatePlayingMessageProgress(); @@ -7498,8 +7612,6 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } radialProgress.setProgress(setProgress, false); - radialProgress.setIcon(getIconForCurrentState(), ifSame, animated); - invalidate(); } else { DownloadController.getInstance(currentAccount).removeLoadingFileObserver(this); if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && !photoImage.isAllowStartAnimation()) { @@ -7507,9 +7619,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { buttonState = -1; } - radialProgress.setIcon(getIconForCurrentState(), ifSame, animated); - invalidate(); } + radialProgress.setIcon(getIconForCurrentState(), ifSame, animated); + invalidate(); } else { if (currentMessageObject.isOut() && (currentMessageObject.isSending() || currentMessageObject.isEditing())) { if (!TextUtils.isEmpty(currentMessageObject.messageOwner.attachPath)) { @@ -8008,11 +8120,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (!instant && currentMessageObject.isOut()) { MessagesController.DiceFrameSuccess frameSuccess = MessagesController.getInstance(currentAccount).diceSuccess.get(emoji); if (frameSuccess != null && frameSuccess.num == value) { - lottieDrawable.setOnFinishCallback(() -> { - if (delegate != null) { - delegate.onDiceFinished(); - } - }, frameSuccess.frame); + lottieDrawable.setOnFinishCallback(diceFinishCallback, frameSuccess.frame); } } TLRPC.Document document = stickerSet.documents.get(Math.max(value, 0)); @@ -8660,6 +8768,14 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else if (messageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaInvoice) { stringFinalText = Emoji.replaceEmoji(messageObject.replyMessageObject.messageOwner.media.title, Theme.chat_replyTextPaint.getFontMetricsInt(), AndroidUtilities.dp(14), false); stringFinalText = TextUtils.ellipsize(stringFinalText, Theme.chat_replyTextPaint, maxWidth, TextUtils.TruncateAt.END); + } else if (!TextUtils.isEmpty(messageObject.replyMessageObject.caption)) { + String mess = messageObject.replyMessageObject.caption.toString(); + if (mess.length() > 150) { + mess = mess.substring(0, 150); + } + mess = mess.replace('\n', ' '); + stringFinalText = Emoji.replaceEmoji(mess, Theme.chat_replyTextPaint.getFontMetricsInt(), AndroidUtilities.dp(14), false); + stringFinalText = TextUtils.ellipsize(stringFinalText, Theme.chat_replyTextPaint, maxWidth, TextUtils.TruncateAt.END); } else if (messageObject.replyMessageObject.messageText != null && messageObject.replyMessageObject.messageText.length() > 0) { String mess = messageObject.replyMessageObject.messageText.toString(); if (mess.length() > 150) { @@ -8700,7 +8816,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate String fromFormattedString = LocaleController.getString("FromFormatted", R.string.FromFormatted); int idx = fromFormattedString.indexOf("%1$s"); int fromWidth = (int) Math.ceil(Theme.chat_replyNamePaint.measureText(from + " ")); - CharSequence n = TextUtils.ellipsize(currentForwardNameString.replace('\n', ' '), Theme.chat_replyNamePaint, maxWidth - fromWidth, TextUtils.TruncateAt.END); + CharSequence n = TextUtils.ellipsize(currentForwardNameString == null ? "" : currentForwardNameString.replace('\n', ' '), Theme.chat_replyNamePaint, maxWidth - fromWidth, TextUtils.TruncateAt.END); SpannableStringBuilder stringBuilder = new SpannableStringBuilder(String.format(fromFormattedString, n)); if (idx >= 0 && (currentForwardName == null || messageObject.messageOwner.fwd_from.from_id != 0)) { stringBuilder.setSpan(new TypefaceSpan(AndroidUtilities.getTypeface("fonts/rmedium.ttf")), idx, idx + n.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -8709,7 +8825,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate forwardNameCenterX = fromWidth + (int) Math.ceil(Theme.chat_replyNamePaint.measureText(n, 0, n.length())) / 2; } } - CharSequence stringFinalName = TextUtils.ellipsize(name.replace('\n', ' '), Theme.chat_replyNamePaint, maxWidth, TextUtils.TruncateAt.END); + CharSequence stringFinalName = name == null ? "" : TextUtils.ellipsize(name.replace('\n', ' '), Theme.chat_replyNamePaint, maxWidth, TextUtils.TruncateAt.END); try { replyNameWidth = AndroidUtilities.dp(4 + (needReplyImage ? 44 : 0)); @@ -8820,7 +8936,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate h = view.getMeasuredHeight(); } } - drawable.setTop((int) getY(), h, pinnedTop, pinnedBottom); + drawable.setTop((int) getY(), h, pinnedTop, pinnedBottom || transitionParams.changePinnedBottomProgress != 1); } } @@ -8899,7 +9015,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate int additionalTop = 0; int additionalBottom = 0; if (currentMessageObject.isOutOwner()) { - if (!mediaBackground && !drawPinnedBottom) { + if (transitionParams.changePinnedBottomProgress != 1) { + currentBackgroundDrawable = Theme.chat_msgOutMediaDrawable; + currentBackgroundSelectedDrawable = Theme.chat_msgOutMediaSelectedDrawable; + transitionParams.drawPinnedBottomBackground = true; + } else if (!mediaBackground && !drawPinnedBottom) { currentBackgroundDrawable = Theme.chat_msgOutDrawable; currentBackgroundSelectedDrawable = Theme.chat_msgOutSelectedDrawable; transitionParams.drawPinnedBottomBackground = false; @@ -8922,7 +9042,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } int backgroundLeft = backgroundDrawableLeft; - if (!mediaBackground && drawPinnedBottom) { + if (transitionParams.changePinnedBottomProgress != 1) { + backgroundDrawableRight -= AndroidUtilities.dp(6); + } else if (!mediaBackground && drawPinnedBottom) { backgroundDrawableRight -= AndroidUtilities.dp(6); } @@ -8957,7 +9079,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate setDrawableBoundsInner(currentBackgroundSelectedDrawable, backgroundLeft, backgroundDrawableTop, backgroundDrawableRight, backgroundHeight); setDrawableBoundsInner(currentBackgroundShadowDrawable, backgroundLeft, backgroundDrawableTop, backgroundDrawableRight, backgroundHeight); } else { - if (!mediaBackground && !drawPinnedBottom) { + if (transitionParams.changePinnedBottomProgress != 1) { + currentBackgroundDrawable = Theme.chat_msgInMediaDrawable; + currentBackgroundSelectedDrawable = Theme.chat_msgInMediaSelectedDrawable; + transitionParams.drawPinnedBottomBackground = true; + } else if (!mediaBackground && !drawPinnedBottom) { currentBackgroundDrawable = Theme.chat_msgInDrawable; currentBackgroundSelectedDrawable = Theme.chat_msgInSelectedDrawable; transitionParams.drawPinnedBottomBackground = false; @@ -8984,7 +9110,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate backgroundDrawableLeft += (int) Math.ceil(currentPosition.leftSpanOffset / 1000.0f * getGroupPhotosWidth()); } } - if (!mediaBackground && drawPinnedBottom) { + if ((!mediaBackground && drawPinnedBottom) || transitionParams.changePinnedBottomProgress != 1) { backgroundDrawableRight -= AndroidUtilities.dp(6); backgroundDrawableLeft += AndroidUtilities.dp(6); } @@ -9072,16 +9198,14 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (checkBoxAnimationProgress > 1.0f) { checkBoxAnimationProgress = 1.0f; } - invalidate(); - ((View) getParent()).invalidate(); } else { checkBoxAnimationProgress -= dt / 200.0f; if (checkBoxAnimationProgress <= 0.0f) { checkBoxAnimationProgress = 0.0f; } - invalidate(); - ((View) getParent()).invalidate(); } + invalidate(); + ((View) getParent()).invalidate(); } } @@ -9114,16 +9238,12 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (transitionParams.changePinnedBottomProgress != 1f) { if (currentMessageObject.isOutOwner()) { - Theme.MessageDrawable drawable; - if (transitionParams.drawPinnedBottomBackground) { - drawable = Theme.chat_msgOutDrawable; - } else { - drawable = Theme.chat_msgOutMediaDrawable; - } + Theme.MessageDrawable drawable = Theme.chat_msgOutDrawable; + Rect rect = currentBackgroundDrawable.getBounds(); drawable.setBounds(rect.left, rect.top, rect.right + AndroidUtilities.dp(6), rect.bottom); canvas.save(); - canvas.clipRect(rect.right - AndroidUtilities.dp(6), rect.bottom - AndroidUtilities.dp(16), rect.right + AndroidUtilities.dp(6), rect.bottom); + canvas.clipRect(rect.right - AndroidUtilities.dp(12), rect.bottom - AndroidUtilities.dp(16), rect.right + AndroidUtilities.dp(12), rect.bottom); int h = parentHeight; if (h == 0) { h = AndroidUtilities.displaySize.y; @@ -9133,7 +9253,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } drawable.setTop((int) getY(), h, pinnedTop, pinnedBottom); - drawable.setAlpha((int) (255 * (1f - transitionParams.changePinnedBottomProgress))); + float alpha = !mediaBackground && !pinnedBottom ? transitionParams.changePinnedBottomProgress : (1f - transitionParams.changePinnedBottomProgress); + drawable.setAlpha((int) (255 * alpha)); drawable.draw(canvas); drawable.setAlpha(255); canvas.restore(); @@ -9144,7 +9265,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { drawable = Theme.chat_msgInMediaDrawable; } - drawable.setAlpha((int) (255 * (1f - transitionParams.changePinnedBottomProgress))); + float alpha = !mediaBackground && !pinnedBottom ? transitionParams.changePinnedBottomProgress : (1f - transitionParams.changePinnedBottomProgress); + drawable.setAlpha((int) (255 * alpha)); Rect rect = currentBackgroundDrawable.getBounds(); drawable.setBounds(rect.left - AndroidUtilities.dp(6), rect.top, rect.right, rect.bottom); canvas.save(); @@ -9216,6 +9338,15 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate canvas.restore(); } + if (!transitionParams.animateBackgroundBoundsInner) { + if (!transitionParams.transitionBotButtons.isEmpty()) { + drawBotButtons(canvas, transitionParams.transitionBotButtons, 1f - transitionParams.animateChangeProgress); + } + if (!botButtons.isEmpty()) { + drawBotButtons(canvas, botButtons, transitionParams.animateChangeProgress); + } + } + if (drawShareButton) { if (sharePressed) { if (!Theme.isCustomTheme() || Theme.hasThemeKey(Theme.key_chat_shareBackgroundSelected)) { @@ -9322,6 +9453,17 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate updateSelectionTextPosition(); } + public void drawOutboundsContent(Canvas canvas) { + if (transitionParams.animateBackgroundBoundsInner) { + if (!transitionParams.transitionBotButtons.isEmpty()) { + drawBotButtons(canvas, transitionParams.transitionBotButtons, 1f - transitionParams.animateChangeProgress); + } + if (!botButtons.isEmpty()) { + drawBotButtons(canvas, botButtons, transitionParams.animateChangeProgress); + } + } + } + public void setTimeAlpha(float value) { timeAlpha = value; } @@ -9414,24 +9556,19 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate Drawable currentBackgroundShadowDrawable = null; if (currentBackgroundDrawable != null) { - currentBackgroundDrawable.setBounds(left,top,right,bottom); currentBackgroundShadowDrawable = currentBackgroundDrawable.getShadowDrawable(); } - if (currentBackgroundSelectedDrawable != null) { - currentBackgroundSelectedDrawable.setBounds(left,top,right,bottom); - } - if (currentBackgroundShadowDrawable != null) { - currentBackgroundShadowDrawable.setBounds(left,top,right,bottom); - } if (currentBackgroundShadowDrawable != null) { currentBackgroundShadowDrawable.setAlpha((int) (getAlpha() * 255)); + currentBackgroundShadowDrawable.setBounds(left, top, right, bottom); currentBackgroundShadowDrawable.draw(canvas); currentBackgroundShadowDrawable.setAlpha(255); } if (currentBackgroundDrawable != null) { currentBackgroundDrawable.setAlpha((int) (getAlpha() * 255)); + currentBackgroundDrawable.setBounds(left, top, right, bottom); currentBackgroundDrawable.draw(canvas); currentBackgroundDrawable.setAlpha(255); } @@ -9621,7 +9758,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (currentMessageObject.isOutOwner()) { Theme.chat_replyLinePaint.setColor(Theme.getColor(Theme.key_chat_outReplyLine)); Theme.chat_replyNamePaint.setColor(Theme.getColor(Theme.key_chat_outReplyNameText)); - if (currentMessageObject.hasValidReplyMessageObject() && currentMessageObject.replyMessageObject.type == 0 && !(currentMessageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGame || currentMessageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaInvoice)) { + if (currentMessageObject.hasValidReplyMessageObject() && (currentMessageObject.replyMessageObject.type == 0 || !TextUtils.isEmpty(currentMessageObject.replyMessageObject.caption)) && !(currentMessageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGame || currentMessageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaInvoice)) { Theme.chat_replyTextPaint.setColor(Theme.getColor(Theme.key_chat_outReplyMessageText)); } else { Theme.chat_replyTextPaint.setColor(Theme.getColor(isDrawSelectionBackground() ? Theme.key_chat_outReplyMediaMessageSelectedText : Theme.key_chat_outReplyMediaMessageText)); @@ -9629,7 +9766,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { Theme.chat_replyLinePaint.setColor(Theme.getColor(Theme.key_chat_inReplyLine)); Theme.chat_replyNamePaint.setColor(Theme.getColor(Theme.key_chat_inReplyNameText)); - if (currentMessageObject.hasValidReplyMessageObject() && currentMessageObject.replyMessageObject.type == 0 && !(currentMessageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGame || currentMessageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaInvoice)) { + if (currentMessageObject.hasValidReplyMessageObject() && (currentMessageObject.replyMessageObject.type == 0 || !TextUtils.isEmpty(currentMessageObject.replyMessageObject.caption)) && !(currentMessageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGame || currentMessageObject.replyMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaInvoice)) { Theme.chat_replyTextPaint.setColor(Theme.getColor(Theme.key_chat_inReplyMessageText)); } else { Theme.chat_replyTextPaint.setColor(Theme.getColor(isDrawSelectionBackground() ? Theme.key_chat_inReplyMediaMessageSelectedText : Theme.key_chat_inReplyMediaMessageText)); @@ -9853,6 +9990,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } } + if (getTransitionParams().animateDrawingTimeAlpha) { + alpha *= getTransitionParams().animateChangeProgress; + } if (alpha != 1f) { Theme.chat_timePaint.setAlpha((int) (Theme.chat_timePaint.getAlpha() * alpha)); } @@ -10028,11 +10168,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else if (currentMessageObject.isSent()) { if (!currentMessageObject.scheduled && !currentMessageObject.isUnread()) { drawCheck1 = true; - drawCheck2 = true; } else { drawCheck1 = false; - drawCheck2 = true; } + drawCheck2 = true; drawClock = false; drawError = false; } @@ -10169,7 +10308,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } - if (currentMessageObject.type == MessageObject.TYPE_VIDEO || currentMessageObject.type == MessageObject.TYPE_PHOTO || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { + if (hasGamePreview) { + + } else if (currentMessageObject.type == MessageObject.TYPE_VIDEO || currentMessageObject.type == MessageObject.TYPE_PHOTO || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { if (photoImage.getVisible()) { if (!currentMessageObject.needDrawBluredPreview()) { if (documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { @@ -10888,7 +11029,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate public boolean performAccessibilityAction(int action, Bundle arguments) { if (action == AccessibilityNodeInfo.ACTION_CLICK) { int icon = getIconForCurrentState(); - if (icon != MediaActionDrawable.ICON_NONE) { + if (icon != MediaActionDrawable.ICON_NONE && icon != MediaActionDrawable.ICON_FILE) { didPressButton(true, false); } else if (currentMessageObject.type == 16) { delegate.didPressOther(this, otherX, otherY); @@ -10907,6 +11048,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } } + if (currentMessageObject.isVoice() || currentMessageObject.isMusic() && MediaController.getInstance().isPlayingMessage(currentMessageObject)) { + if (seekBarAccessibilityDelegate.performAccessibilityActionInternal(action, arguments)) { + return true; + } + } return super.performAccessibilityAction(action, arguments); } @@ -11118,6 +11264,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } private void updateTranslation() { + if (currentMessageObject == null) { + return; + } int checkBoxOffset = !currentMessageObject.isOutOwner() ? checkBoxTranslation : 0; setTranslationX(slidingOffsetX + animationOffsetX + checkBoxOffset); } @@ -11179,13 +11328,25 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (!TextUtils.isEmpty(currentMessageObject.messageText)) { sb.append(currentMessageObject.messageText); } + if (documentAttach != null && (documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT || documentAttachType == DOCUMENT_ATTACH_TYPE_GIF || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO)) { + if (buttonState == 1 && loadingProgressLayout != null) { + sb.append("\n"); + final boolean sending = currentMessageObject.isSending(); + final String key = sending ? "AccDescrUploadProgress" : "AccDescrDownloadProgress"; + final int resId = sending ? R.string.AccDescrUploadProgress : R.string.AccDescrDownloadProgress; + sb.append(LocaleController.formatString(key, resId, AndroidUtilities.formatFileSize(currentMessageObject.loadedFileSize), AndroidUtilities.formatFileSize(lastLoadingSizeTotal))); + } else if (buttonState == 0 || documentAttachType == DOCUMENT_ATTACH_TYPE_DOCUMENT) { + sb.append(", "); + sb.append(AndroidUtilities.formatFileSize(documentAttach.size)); + } + } if (currentMessageObject.isMusic()) { sb.append("\n"); sb.append(LocaleController.formatString("AccDescrMusicInfo", R.string.AccDescrMusicInfo, currentMessageObject.getMusicAuthor(), currentMessageObject.getMusicTitle())); } else if (currentMessageObject.isVoice() || currentMessageObject.isRoundVideo()){ sb.append(", "); - sb.append(LocaleController.formatCallDuration(currentMessageObject.getDuration())); - if (currentMessageObject.isContentUnread()){ + sb.append(LocaleController.formatDuration(currentMessageObject.getDuration())); + if (currentMessageObject.isContentUnread()) { sb.append(", "); sb.append(LocaleController.getString("AccDescrMsgNotPlayed", R.string.AccDescrMsgNotPlayed)); } @@ -11216,15 +11377,36 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate sb.append("\n"); sb.append(currentMessageObject.caption); } - sb.append("\n"); - CharSequence time = LocaleController.getString("TodayAt", R.string.TodayAt) + " " + currentTimeString; if (currentMessageObject.isOut()) { - sb.append(LocaleController.formatString("AccDescrSentDate", R.string.AccDescrSentDate, time)); - sb.append(", "); - sb.append(currentMessageObject.isUnread() ? LocaleController.getString("AccDescrMsgUnread", R.string.AccDescrMsgUnread) : LocaleController.getString("AccDescrMsgRead", R.string.AccDescrMsgRead)); + if (currentMessageObject.isSent()) { + sb.append("\n"); + if (currentMessageObject.scheduled) { + sb.append(LocaleController.formatString("AccDescrScheduledDate", R.string.AccDescrScheduledDate, currentTimeString)); + } else { + sb.append(LocaleController.formatString("AccDescrSentDate", R.string.AccDescrSentDate, LocaleController.getString("TodayAt", R.string.TodayAt) + " " + currentTimeString)); + sb.append(", "); + sb.append(currentMessageObject.isUnread() ? LocaleController.getString("AccDescrMsgUnread", R.string.AccDescrMsgUnread) : LocaleController.getString("AccDescrMsgRead", R.string.AccDescrMsgRead)); + } + } else if (currentMessageObject.isSending()) { + sb.append("\n"); + sb.append(LocaleController.getString("AccDescrMsgSending", R.string.AccDescrMsgSending)); + final float sendingProgress = radialProgress.getProgress(); + if (sendingProgress > 0f) { + sb.append(", ").append(Math.round(sendingProgress * 100)).append("%"); + } + } else if (currentMessageObject.isSendError()) { + sb.append("\n"); + sb.append(LocaleController.getString("AccDescrMsgSendingError", R.string.AccDescrMsgSendingError)); + } } else { - sb.append(LocaleController.formatString("AccDescrReceivedDate", R.string.AccDescrReceivedDate, time)); + sb.append("\n"); + sb.append(LocaleController.formatString("AccDescrReceivedDate", R.string.AccDescrReceivedDate, LocaleController.getString("TodayAt", R.string.TodayAt) + " " + currentTimeString)); } + if ((currentMessageObject.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { + sb.append("\n"); + sb.append(LocaleController.formatPluralString("AccDescrNumberOfViews", currentMessageObject.messageOwner.views)); + } + sb.append("\n"); info.setContentDescription(sb.toString()); info.setEnabled(true); if (Build.VERSION.SDK_INT >= 19) { @@ -11269,6 +11451,10 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK); } + if (currentMessageObject.isVoice() || currentMessageObject.isMusic() && MediaController.getInstance().isPlayingMessage(currentMessageObject)) { + seekBarAccessibilityDelegate.onInitializeAccessibilityNodeInfoInternal(info); + } + int i; if (currentMessageObject.messageText instanceof Spannable) { Spannable buffer = (Spannable) currentMessageObject.messageText; @@ -11289,7 +11475,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate info.addChild(ChatMessageCell.this, POLL_BUTTONS_START + i); i++; } - if (drawInstantView) { + if (drawInstantView && !instantButtonRect.isEmpty()) { info.addChild(ChatMessageCell.this, INSTANT_VIEW); } if (drawShareButton) { @@ -11398,15 +11584,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate info.setText(instantViewLayout.getText()); } info.addAction(AccessibilityNodeInfo.ACTION_CLICK); - int textX = (int) photoImage.getImageX(); - int instantY = getMeasuredHeight() - AndroidUtilities.dp(36 + 28); - int addX; - if (currentMessageObject.isOutOwner()) { - addX = getMeasuredWidth() - widthForButtons - AndroidUtilities.dp(10); - } else { - addX = backgroundDrawableLeft + AndroidUtilities.dp(mediaBackground ? 1 : 7); - } - rect.set(textX + addX, instantY, textX + instantWidth + addX, instantY + AndroidUtilities.dp(38)); + instantButtonRect.round(rect); info.setBoundsInParent(rect); if (accessibilityVirtualViewBounds.get(virtualViewId) == null || !accessibilityVirtualViewBounds.get(virtualViewId).equals(rect)) { accessibilityVirtualViewBounds.put(virtualViewId, new Rect(rect)); @@ -11616,8 +11794,17 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private StaticLayout animateOutCaptionLayout; private StaticLayout lastDrawingCaptionLayout; + public boolean animateDrawingTimeAlpha; + public float animateChangeProgress = 1f; - private boolean lastDrawBotButtons; + private ArrayList lastDrawBotButtons = new ArrayList<>(); + private ArrayList transitionBotButtons = new ArrayList<>(); + + private float lastButtonX; + private float lastButtonY; + private float animateFromButtonX; + private float animateFromButtonY; + private boolean animateButton; public void recordDrawingState() { @@ -11638,7 +11825,13 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate lastDrawingCaptionY = captionY; lastDrawingCaptionLayout = captionLayout; - lastDrawBotButtons = !botButtons.isEmpty(); + if (!botButtons.isEmpty()) { + lastDrawBotButtons.clear(); + lastDrawBotButtons.addAll(botButtons); + } + + lastButtonX = buttonX; + lastButtonY = buttonY; } public boolean animateChange() { @@ -11687,6 +11880,18 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate changed = true; } } + if (!lastDrawBotButtons.isEmpty() || !botButtons.isEmpty()) { + transitionBotButtons.addAll(lastDrawBotButtons); + } + + if (documentAttachType == DOCUMENT_ATTACH_TYPE_AUDIO) { + if (buttonX != lastButtonX || buttonY != lastButtonY) { + animateFromButtonX = lastButtonX; + animateFromButtonY = lastButtonY; + animateButton = true; + changed = true; + } + } return changed; } @@ -11725,12 +11930,12 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate transformGroupToSingleMessage = false; animateOutCaptionLayout = null; moveCaption = false; + animateDrawingTimeAlpha = false; + transitionBotButtons.clear(); + animateButton = false; } public boolean supportChangeAnimation() { - if (lastDrawBotButtons || !botButtons.isEmpty()) { - return false; - } return true; } } 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 80e6db55e..655a24911 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java @@ -326,7 +326,7 @@ public class ContextLinkCell extends FrameLayout implements DownloadController.F if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { if (documentAttach != null) { - TLRPC.TL_videoSize thumb = MessageObject.getDocumentVideoThumb(documentAttach); + TLRPC.VideoSize thumb = MessageObject.getDocumentVideoThumb(documentAttach); if (thumb != null) { linkImageView.setImage(ImageLocation.getForDocument(thumb, documentAttach), null, ImageLocation.getForDocument(currentPhotoObject, documentAttach), currentPhotoFilter, -1, ext, parentObject, 1); } else { @@ -987,10 +987,6 @@ public class ContextLinkCell extends FrameLayout implements DownloadController.F break; case DOCUMENT_ATTACH_TYPE_MUSIC: sbuf.append(LocaleController.getString("AttachMusic", R.string.AttachMusic)); - if (descriptionLayout != null && titleLayout != null) { - sbuf.append(", "); - sbuf.append(LocaleController.formatString("AccDescrMusicInfo", R.string.AccDescrMusicInfo, descriptionLayout.getText(), titleLayout.getText())); - } break; case DOCUMENT_ATTACH_TYPE_STICKER: sbuf.append(LocaleController.getString("AttachSticker", R.string.AttachSticker)); @@ -1001,16 +997,25 @@ public class ContextLinkCell extends FrameLayout implements DownloadController.F case DOCUMENT_ATTACH_TYPE_GEO: sbuf.append(LocaleController.getString("AttachLocation", R.string.AttachLocation)); break; - default: - if (titleLayout != null && !TextUtils.isEmpty(titleLayout.getText())) { - sbuf.append(titleLayout.getText()); + } + final boolean hasTitle = titleLayout != null && !TextUtils.isEmpty(titleLayout.getText()); + final boolean hasDescription = descriptionLayout != null && !TextUtils.isEmpty(descriptionLayout.getText()); + if (documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC && hasTitle && hasDescription) { + sbuf.append(", "); + sbuf.append(LocaleController.formatString("AccDescrMusicInfo", R.string.AccDescrMusicInfo, descriptionLayout.getText(), titleLayout.getText())); + } else { + if (hasTitle) { + if (sbuf.length() > 0) { + sbuf.append(", "); } - if (descriptionLayout != null && !TextUtils.isEmpty(descriptionLayout.getText())) { - if (sbuf.length() > 0) - sbuf.append(", "); - sbuf.append(descriptionLayout.getText()); + sbuf.append(titleLayout.getText()); + } + if (hasDescription) { + if (sbuf.length() > 0) { + sbuf.append(", "); } - break; + sbuf.append(descriptionLayout.getText()); + } } info.setText(sbuf); if (checkBox != null && checkBox.isChecked()) { 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 7c6089a8a..c813e8997 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java @@ -31,8 +31,8 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Interpolator; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.BuildVars; import org.telegram.messenger.ChatObject; +import org.telegram.messenger.DownloadController; import org.telegram.messenger.FileLoader; import org.telegram.messenger.MediaDataController; import org.telegram.messenger.DialogObject; @@ -67,12 +67,11 @@ import tw.nekomimi.nekogram.NekoConfig; public class DialogCell extends BaseCell { - - public static class FixedWidthSize extends ReplacementSpan { + public static class FixedWidthSpan extends ReplacementSpan { private int width; - public FixedWidthSize(int w) { + public FixedWidthSpan(int w) { width = w; } @@ -150,11 +149,9 @@ public class DialogCell extends BaseCell { private float currentRevealBounceProgress; private float archiveBackgroundProgress; - private static String[] newLine = new String[]{"\n"}; - private static String[] space = new String[]{" "}; - private boolean hasMessageThumb; private ImageReceiver thumbImage = new ImageReceiver(this); + private boolean drawPlay; private ImageReceiver avatarImage = new ImageReceiver(this); private AvatarDrawable avatarDrawable = new AvatarDrawable(); @@ -847,17 +844,25 @@ public class DialogCell extends BaseCell { currentMessagePaint = Theme.dialogs_messagePrintingPaint[paintIndex]; } else { boolean needEmoji = true; - if ((BuildVars.DEBUG_VERSION || NekoConfig.mediaPreview) && encryptedChat == null && !message.needDrawBluredPreview() && (message.isPhoto() || message.isNewGif() || message.isVideo())) { - TLRPC.PhotoSize smallThumb = FileLoader.getClosestPhotoSizeWithSize(message.photoThumbs, 40); - TLRPC.PhotoSize bigThumb = FileLoader.getClosestPhotoSizeWithSize(message.photoThumbs, AndroidUtilities.getPhotoSize()); - if (smallThumb == bigThumb) { - bigThumb = null; - } - if (smallThumb != null) { - hasMessageThumb = true; - //TODO respect autodownload settings? - thumbImage.setImage(ImageLocation.getForObject(bigThumb, message.photoThumbsObject), "20_20", ImageLocation.getForObject(smallThumb, message.photoThumbsObject), "20_20", null, message, 0); - needEmoji = false; + if (currentDialogFolderId == 0 && encryptedChat == null && !message.needDrawBluredPreview() && (message.isPhoto() || message.isNewGif() || message.isVideo())) { + String type = message.isWebpage() ? message.messageOwner.media.webpage.type : null; + if (!("app".equals(type) || "profile".equals(type) || "article".equals(type))) { + TLRPC.PhotoSize smallThumb = FileLoader.getClosestPhotoSizeWithSize(message.photoThumbs, 40); + TLRPC.PhotoSize bigThumb = FileLoader.getClosestPhotoSizeWithSize(message.photoThumbs, AndroidUtilities.getPhotoSize()); + if (smallThumb == bigThumb) { + bigThumb = null; + } + if (smallThumb != null) { + hasMessageThumb = true; + drawPlay = message.isVideo(); + String fileName = FileLoader.getAttachFileName(bigThumb); + if (message.mediaExists || DownloadController.getInstance(currentAccount).canDownloadMedia(message) || FileLoader.getInstance(currentAccount).isLoadingFile(fileName)) { + thumbImage.setImage(ImageLocation.getForObject(bigThumb, message.photoThumbsObject), "20_20", ImageLocation.getForObject(smallThumb, message.photoThumbsObject), "20_20", null, message, 0); + } else { + thumbImage.setImage(null, null, ImageLocation.getForObject(smallThumb, message.photoThumbsObject), "20_20", null, message, 0); + } + needEmoji = false; + } } } if (chat != null && chat.id > 0 && fromChat == null) { @@ -958,7 +963,7 @@ public class DialogCell extends BaseCell { checkMessage = false; SpannableStringBuilder builder = (SpannableStringBuilder) messageString; builder.insert(thumbInsertIndex, " "); - builder.setSpan(new FixedWidthSize(AndroidUtilities.dp(thumbSize + 6)), thumbInsertIndex, thumbInsertIndex + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setSpan(new FixedWidthSpan(AndroidUtilities.dp(thumbSize + 6)), thumbInsertIndex, thumbInsertIndex + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } else { if (message.messageOwner.media instanceof TLRPC.TL_messageMediaPhoto && message.messageOwner.media.photo instanceof TLRPC.TL_photoEmpty && message.messageOwner.media.ttl_seconds != 0) { @@ -1000,14 +1005,14 @@ public class DialogCell extends BaseCell { if (messageString.length() > 150) { messageString = messageString.subSequence(0, 150); } - messageString = TextUtils.replace(messageString, newLine, space); + messageString = AndroidUtilities.replaceNewLines(messageString); if (!(messageString instanceof SpannableStringBuilder)) { messageString = new SpannableStringBuilder(messageString); } checkMessage = false; SpannableStringBuilder builder = (SpannableStringBuilder) messageString; builder.insert(0, " "); - builder.setSpan(new FixedWidthSize(AndroidUtilities.dp(thumbSize + 6)), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + builder.setSpan(new FixedWidthSpan(AndroidUtilities.dp(thumbSize + 6)), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Emoji.replaceEmoji(builder, Theme.dialogs_messagePaint[paintIndex].getFontMetricsInt(), AndroidUtilities.dp(17), false); } } @@ -1393,12 +1398,15 @@ public class DialogCell extends BaseCell { } if (useForceThreeLines || SharedConfig.useThreeLinesLayout) { if (hasMessageThumb && messageNameString != null) { - messageWidth += AndroidUtilities.dp(4); + messageWidth += AndroidUtilities.dp(6); } messageLayout = StaticLayoutEx.createStaticLayout(messageStringFinal, currentMessagePaint, messageWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, AndroidUtilities.dp(1), false, TextUtils.TruncateAt.END, messageWidth, messageNameString != null ? 1 : 2); } else { if (hasMessageThumb) { - messageWidth += AndroidUtilities.dp(4); + messageWidth += thumbSize + AndroidUtilities.dp(6); + if (LocaleController.isRTL) { + messageLeft -= thumbSize + AndroidUtilities.dp(6); + } } messageLayout = new StaticLayout(messageStringFinal, currentMessagePaint, messageWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); } @@ -2188,6 +2196,12 @@ public class DialogCell extends BaseCell { if (hasMessageThumb) { thumbImage.draw(canvas); + if (drawPlay) { + int x = (int) (thumbImage.getCenterX() - Theme.dialogs_playDrawable.getIntrinsicWidth() / 2); + int y = (int) (thumbImage.getCenterY() - Theme.dialogs_playDrawable.getIntrinsicHeight() / 2); + setDrawableBounds(Theme.dialogs_playDrawable, x, y); + Theme.dialogs_playDrawable.draw(canvas); + } } if (animatingArchiveAvatar) { 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 72e5ab12f..8c44aad77 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java @@ -173,7 +173,7 @@ public class DrawerProfileCell extends FrameLayout { Bitmap bitmap = ((BitmapDrawable) backgroundDrawable).getBitmap(); float scaleX = (float) getMeasuredWidth() / (float) bitmap.getWidth(); float scaleY = (float) getMeasuredHeight() / (float) bitmap.getHeight(); - float scale = scaleX < scaleY ? scaleY : scaleX; + float scale = Math.max(scaleX, scaleY); int width = (int) (getMeasuredWidth() / scale); int height = (int) (getMeasuredHeight() / scale); int x = (bitmap.getWidth() - width) / 2; @@ -199,6 +199,14 @@ public class DrawerProfileCell extends FrameLayout { } } + public boolean isInAvatar(float x, float y) { + return x >= avatarImageView.getLeft() && x <= avatarImageView.getRight() && y >= avatarImageView.getTop() && y <= avatarImageView.getBottom(); + } + + public boolean hasAvatar() { + return avatarImageView.getImageReceiver().hasNotThumb(); + } + public boolean isAccountsShown() { return accountsShown; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/GraySectionCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/GraySectionCell.java index 095aba317..5e8ab13c4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/GraySectionCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/GraySectionCell.java @@ -11,9 +11,12 @@ package org.telegram.ui.Cells; import android.content.Context; import android.util.TypedValue; import android.view.Gravity; +import android.widget.Button; import android.widget.FrameLayout; import android.widget.TextView; +import androidx.core.view.ViewCompat; + import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.ui.ActionBar.Theme; @@ -40,11 +43,18 @@ public class GraySectionCell extends FrameLayout { textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 16, 0, 16, 0)); - rightTextView = new TextView(getContext()); + rightTextView = new TextView(getContext()) { + @Override + public CharSequence getAccessibilityClassName() { + return Button.class.getName(); + } + }; rightTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); rightTextView.setTextColor(Theme.getColor(Theme.key_graySectionText)); rightTextView.setGravity((LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL); addView(rightTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.TOP, 16, 0, 16, 0)); + + ViewCompat.setAccessibilityHeading(this, true); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/HeaderCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/HeaderCell.java index ecc06b0cb..bba8c4642 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/HeaderCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/HeaderCell.java @@ -21,6 +21,8 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.core.view.ViewCompat; + import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.ui.ActionBar.Theme; @@ -72,6 +74,7 @@ public class HeaderCell extends LinearLayout { if (!text2) textView2.setVisibility(View.GONE); + ViewCompat.setAccessibilityHeading(this, true); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/MaxFileSizeCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/MaxFileSizeCell.java index 84441c2f3..020a31935 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/MaxFileSizeCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/MaxFileSizeCell.java @@ -20,6 +20,7 @@ import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.FileLoader; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; import org.telegram.ui.ActionBar.Theme; @@ -49,6 +50,7 @@ public class MaxFileSizeCell extends FrameLayout { textView.setSingleLine(true); textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP); textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 21, 13, 21, 0)); sizeTextView = new TextView(context); @@ -58,6 +60,7 @@ public class MaxFileSizeCell extends FrameLayout { sizeTextView.setMaxLines(1); sizeTextView.setSingleLine(true); sizeTextView.setGravity((LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.TOP); + sizeTextView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(sizeTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.TOP, 21, 13, 21, 0)); seekBarView = new SeekBarView(context) { @@ -92,7 +95,7 @@ public class MaxFileSizeCell extends FrameLayout { progress -= 0.25f; size += 90 * 1024 * 1024; - size += 1436 * 1024 * 1024 * (progress / 0.25f); + size += (FileLoader.MAX_FILE_SIZE - size) * (progress / 0.25f); } } } @@ -103,10 +106,18 @@ public class MaxFileSizeCell extends FrameLayout { @Override public void onSeekBarPressed(boolean pressed) { + } + @Override + public CharSequence getContentDescription() { + return textView.getText() + " " + sizeTextView.getText(); } }); + seekBarView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(seekBarView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 38, Gravity.TOP | Gravity.LEFT, 6, 36, 6, 0)); + + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + setAccessibilityDelegate(seekBarView.getSeekBarAccessibilityDelegate()); } protected void didChangedSizeValue(int value) { @@ -185,11 +196,11 @@ public class MaxFileSizeCell extends FrameLayout { progress += 0.25f; size -= 90 * 1024 * 1024; - progress += Math.max(0, size / (float) (1436 * 1024 * 1024)) * 0.25f; + progress += Math.max(0, size / (float) (FileLoader.MAX_FILE_SIZE - 100 * 1024 * 1024)) * 0.25f; } } } - seekBarView.setProgress(progress); + seekBarView.setProgress(Math.min(1.0f, progress)); } public void setEnabled(boolean value, ArrayList animators) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachPhotoCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachPhotoCell.java index dea5ad69e..75ae75ce8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachPhotoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoAttachPhotoCell.java @@ -236,7 +236,7 @@ public class PhotoAttachPhotoCell extends FrameLayout { imageView.setImage(location, null, thumb, searchImage); } else if (searchImage.document != null) { MessageObject.getDocumentVideoThumb(searchImage.document); - TLRPC.TL_videoSize videoSize = MessageObject.getDocumentVideoThumb(searchImage.document); + TLRPC.VideoSize videoSize = MessageObject.getDocumentVideoThumb(searchImage.document); if (videoSize != null) { TLRPC.PhotoSize currentPhotoObject = FileLoader.getClosestPhotoSizeWithSize(searchImage.document.thumbs, 90); imageView.setImage(ImageLocation.getForDocument(videoSize, searchImage.document), null, ImageLocation.getForDocument(currentPhotoObject, searchImage.document), "52_52", null, -1, 1, searchImage); @@ -403,7 +403,7 @@ public class PhotoAttachPhotoCell extends FrameLayout { super.onInitializeAccessibilityNodeInfo(info); info.setEnabled(true); if (photoEntry != null && photoEntry.isVideo) { - info.setText(LocaleController.getString("AttachVideo", R.string.AttachVideo) + ", " + LocaleController.formatCallDuration(photoEntry.duration)); + info.setText(LocaleController.getString("AttachVideo", R.string.AttachVideo) + ", " + LocaleController.formatDuration(photoEntry.duration)); } else { info.setText(LocaleController.getString("AttachPhoto", R.string.AttachPhoto)); } 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 9908fe896..8b8354b97 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java @@ -16,6 +16,7 @@ import android.content.Context; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; +import android.view.View; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.TextView; @@ -36,9 +37,9 @@ public class PhotoEditToolCell extends FrameLayout { valueTextView.setTag(null); valueAnimation = new AnimatorSet(); valueAnimation.playTogether( - ObjectAnimator.ofFloat(valueTextView, "alpha", 0.0f), - ObjectAnimator.ofFloat(nameTextView, "alpha", 1.0f)); - valueAnimation.setDuration(180); + ObjectAnimator.ofFloat(valueTextView, View.ALPHA, 0.0f), + ObjectAnimator.ofFloat(nameTextView, View.ALPHA, 1.0f)); + valueAnimation.setDuration(250); valueAnimation.setInterpolator(new DecelerateInterpolator()); valueAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -76,37 +77,34 @@ public class PhotoEditToolCell extends FrameLayout { } public void setSeekBarDelegate(final PhotoEditorSeekBar.PhotoEditorSeekBarDelegate photoEditorSeekBarDelegate) { - seekBar.setDelegate(new PhotoEditorSeekBar.PhotoEditorSeekBarDelegate() { - @Override - public void onProgressChanged(int i, int progress) { - photoEditorSeekBarDelegate.onProgressChanged(i, progress); - if (progress > 0) { - valueTextView.setText("+" + progress); - } else { - valueTextView.setText("" + progress); + seekBar.setDelegate((i, progress) -> { + photoEditorSeekBarDelegate.onProgressChanged(i, progress); + if (progress > 0) { + valueTextView.setText("+" + progress); + } else { + valueTextView.setText("" + progress); + } + if (valueTextView.getTag() == null) { + if (valueAnimation != null) { + valueAnimation.cancel(); } - if (valueTextView.getTag() == null) { - if (valueAnimation != null) { - valueAnimation.cancel(); + valueTextView.setTag(1); + valueAnimation = new AnimatorSet(); + valueAnimation.playTogether( + ObjectAnimator.ofFloat(valueTextView, View.ALPHA, 1.0f), + ObjectAnimator.ofFloat(nameTextView, View.ALPHA, 0.0f)); + valueAnimation.setDuration(250); + valueAnimation.setInterpolator(new DecelerateInterpolator()); + valueAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + AndroidUtilities.runOnUIThread(hideValueRunnable, 1000); } - valueTextView.setTag(1); - valueAnimation = new AnimatorSet(); - valueAnimation.playTogether( - ObjectAnimator.ofFloat(valueTextView, "alpha", 1.0f), - ObjectAnimator.ofFloat(nameTextView, "alpha", 0.0f)); - valueAnimation.setDuration(180); - valueAnimation.setInterpolator(new DecelerateInterpolator()); - valueAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - AndroidUtilities.runOnUIThread(hideValueRunnable, 1000); - } - }); - valueAnimation.start(); - } else { - AndroidUtilities.cancelRunOnUIThread(hideValueRunnable); - AndroidUtilities.runOnUIThread(hideValueRunnable, 1000); - } + }); + valueAnimation.start(); + } else { + AndroidUtilities.cancelRunOnUIThread(hideValueRunnable); + AndroidUtilities.runOnUIThread(hideValueRunnable, 1000); } }); } 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 ae6c3ee46..56ce5ca73 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java @@ -140,7 +140,7 @@ public class PhotoPickerPhotoCell extends FrameLayout { if (photoEntry.isVideo) { videoInfoContainer.setVisibility(View.VISIBLE); videoTextView.setText(AndroidUtilities.formatShortDuration(photoEntry.duration)); - setContentDescription(LocaleController.getString("AttachVideo", R.string.AttachVideo) + ", " + LocaleController.formatCallDuration(photoEntry.duration)); + setContentDescription(LocaleController.getString("AttachVideo", R.string.AttachVideo) + ", " + LocaleController.formatDuration(photoEntry.duration)); imageView.setImage("vthumb://" + photoEntry.imageId + ":" + photoEntry.path, null, thumb); } else { videoInfoContainer.setVisibility(View.INVISIBLE); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PollEditTextCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PollEditTextCell.java index 15fa9f65f..a6faec8c4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PollEditTextCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PollEditTextCell.java @@ -166,6 +166,7 @@ public class PollEditTextCell extends FrameLayout { checkBox = new CheckBox2(context, 21); checkBox.setColor(null, Theme.key_windowBackgroundWhiteGrayIcon, Theme.key_checkboxCheck); + checkBox.setContentDescription(LocaleController.getString("AccDescrQuizCorrectAnswer", R.string.AccDescrQuizCorrectAnswer)); checkBox.setDrawUnchecked(true); checkBox.setChecked(true, false); checkBox.setAlpha(0.0f); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedLinkCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedLinkCell.java index 085180cd2..b12be8497 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedLinkCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedLinkCell.java @@ -549,6 +549,7 @@ public class SharedLinkCell extends FrameLayout { sb.append(", "); sb.append(descriptionLayout2.getText()); } + info.setText(sb.toString()); if (checkBox.isChecked()) { info.setChecked(true); info.setCheckable(true); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedMediaSectionCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedMediaSectionCell.java index f19c92d53..f62b22f4f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedMediaSectionCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedMediaSectionCell.java @@ -14,6 +14,8 @@ import android.view.Gravity; import android.widget.FrameLayout; import android.widget.TextView; +import androidx.core.view.ViewCompat; + import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.ui.ActionBar.Theme; @@ -32,6 +34,8 @@ public class SharedMediaSectionCell extends FrameLayout { textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, 16, 0, 16, 0)); + + ViewCompat.setAccessibilityHeading(this, true); } @Override 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 ab25cd562..2028e8007 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java @@ -223,7 +223,7 @@ public class SharedPhotoVideoCell extends FrameLayout { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); if (currentMessageObject.isVideo()) { - info.setText(LocaleController.getString("AttachVideo", R.string.AttachVideo) + ", " + LocaleController.formatCallDuration(currentMessageObject.getDuration())); + info.setText(LocaleController.getString("AttachVideo", R.string.AttachVideo) + ", " + LocaleController.formatDuration(currentMessageObject.getDuration())); } else { info.setText(LocaleController.getString("AttachPhoto", R.string.AttachPhoto)); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharingLiveLocationCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharingLiveLocationCell.java index f1796d756..f3cf1aaaf 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharingLiveLocationCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharingLiveLocationCell.java @@ -193,9 +193,9 @@ public class SharingLiveLocationCell extends FrameLayout { if (userLocation != null) { float distance = location.distanceTo(userLocation); if (address != null) { - distanceTextView.setText(String.format("%s - %s", address, LocaleController.formatDistance(distance))); + distanceTextView.setText(String.format("%s - %s", address, LocaleController.formatDistance(distance, 0))); } else { - distanceTextView.setText(LocaleController.formatDistance(distance)); + distanceTextView.setText(LocaleController.formatDistance(distance, 0)); } } else { if (address != null) { @@ -231,7 +231,7 @@ public class SharingLiveLocationCell extends FrameLayout { String time = LocaleController.formatLocationUpdateDate(info.object.edit_date != 0 ? info.object.edit_date : info.object.date); if (userLocation != null) { - distanceTextView.setText(String.format("%s - %s", time, LocaleController.formatDistance(location.distanceTo(userLocation)))); + distanceTextView.setText(String.format("%s - %s", time, LocaleController.formatDistance(location.distanceTo(userLocation), 0))); } else { distanceTextView.setText(time); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StatisticPostInfoCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StatisticPostInfoCell.java index e77122842..5ad6cfc56 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StatisticPostInfoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StatisticPostInfoCell.java @@ -4,6 +4,7 @@ import android.content.Context; import android.graphics.Color; import android.text.TextUtils; import android.view.Gravity; +import android.view.View; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; @@ -15,6 +16,7 @@ import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.StatisticActivity; @@ -26,6 +28,7 @@ public class StatisticPostInfoCell extends FrameLayout { private TextView shares; private TextView date; private BackupImageView imageView; + private AvatarDrawable avatarDrawable = new AvatarDrawable(); private final TLRPC.ChatFull chat; @@ -70,20 +73,14 @@ public class StatisticPostInfoCell extends FrameLayout { linearLayout.addView(date, LayoutHelper.createLinear(0, LayoutHelper.WRAP_CONTENT, 1f, Gravity.NO_GRAVITY, 0, 0, 8, 0)); linearLayout.addView(shares, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT)); - contentLayout.addView(linearLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.START | Gravity.TOP, 0, 2, 0, 0)); + contentLayout.addView(linearLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.START | Gravity.TOP, 0, 2, 0, 8)); addView(contentLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.NO_GRAVITY, 72, 0, 12, 0)); message.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); views.setTextColor(Theme.getColor(Theme.key_dialogTextBlack)); - date.setTextColor(Theme.getColor(Theme.key_dialogTextGray4)); - shares.setTextColor(Theme.getColor(Theme.key_dialogTextGray4)); - } - - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(56), MeasureSpec.EXACTLY)); + date.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText3)); + shares.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText3)); } public void setData(StatisticActivity.RecentPostInfo postInfo) { @@ -112,4 +109,15 @@ public class StatisticPostInfoCell extends FrameLayout { date.setText(LocaleController.formatDateAudio(postInfo.message.messageOwner.date, false)); shares.setText(String.format(LocaleController.getPluralString("Shares", postInfo.counters.forwards), AndroidUtilities.formatCount(postInfo.counters.forwards))); } + + public void setData(StatisticActivity.MemberData memberData) { + avatarDrawable.setInfo(memberData.user); + imageView.setImage(ImageLocation.getForUser(memberData.user, false), "50_50", avatarDrawable, memberData.user); + imageView.setRoundRadius(AndroidUtilities.dp(46) >> 1); + message.setText(memberData.user.first_name); + date.setText(memberData.description); + + views.setVisibility(View.GONE); + shares.setVisibility(View.GONE); + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCell.java index 88f3ec1b4..9e3ee7758 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCell.java @@ -13,7 +13,9 @@ import android.graphics.Canvas; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; +import android.text.TextUtils; import android.view.Gravity; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.ImageView; @@ -46,12 +48,14 @@ public class TextCell extends FrameLayout { textView.setTextColor(Theme.getColor(dialog ? Theme.key_dialogTextBlack : Theme.key_windowBackgroundWhiteBlackText)); textView.setTextSize(16); textView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); + textView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(textView); valueTextView = new SimpleTextView(context); valueTextView.setTextColor(Theme.getColor(dialog ? Theme.key_dialogTextBlue2 : Theme.key_windowBackgroundWhiteValueText)); valueTextView.setTextSize(16); valueTextView.setGravity(LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT); + valueTextView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(valueTextView); imageView = new ImageView(context); @@ -221,4 +225,18 @@ public class TextCell extends FrameLayout { canvas.drawLine(0, getMeasuredHeight() - 1, getMeasuredWidth(), getMeasuredHeight() - 1, Theme.dividerPaint); } } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + final CharSequence text = textView.getText(); + if (!TextUtils.isEmpty(text)) { + final CharSequence valueText = valueTextView.getText(); + if (!TextUtils.isEmpty(valueText)) { + info.setText(text + ": " + valueText); + } else { + info.setText(text); + } + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailCell.java index f3e321cd2..638772d3c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailCell.java @@ -13,6 +13,7 @@ import android.graphics.Canvas; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.TextView; @@ -27,6 +28,7 @@ public class TextDetailCell extends FrameLayout { private TextView textView; private TextView valueTextView; private boolean needDivider; + private boolean contentDescriptionValueFirst; public TextDetailCell(Context context) { super(context); @@ -39,6 +41,7 @@ public class TextDetailCell extends FrameLayout { textView.setMaxLines(1); textView.setSingleLine(true); textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(textView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 23, 8, 23, 0)); valueTextView = new TextView(context); @@ -48,6 +51,7 @@ public class TextDetailCell extends FrameLayout { valueTextView.setMaxLines(1); valueTextView.setSingleLine(true); valueTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); + valueTextView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(valueTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 23, 33, 23, 0)); } @@ -70,6 +74,10 @@ public class TextDetailCell extends FrameLayout { setWillNotDraw(!divider); } + public void setContentDescriptionValueFirst(boolean contentDescriptionValueFirst) { + this.contentDescriptionValueFirst = contentDescriptionValueFirst; + } + @Override public void invalidate() { super.invalidate(); @@ -82,4 +90,14 @@ public class TextDetailCell extends FrameLayout { canvas.drawLine(0, getMeasuredHeight() - 1, getMeasuredWidth(), getMeasuredHeight() - 1, Theme.dividerPaint); } } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + final CharSequence text = textView.getText(); + final CharSequence valueText = valueTextView.getText(); + if (!TextUtils.isEmpty(text) && !TextUtils.isEmpty(valueText)) { + info.setText((contentDescriptionValueFirst ? valueText : text) + ": " + (contentDescriptionValueFirst ? text : valueText)); + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextInfoPrivacyCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextInfoPrivacyCell.java index 6f5f53b5d..59dce36d0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextInfoPrivacyCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextInfoPrivacyCell.java @@ -17,6 +17,7 @@ import android.text.method.LinkMovementMethod; import android.text.style.AbsoluteSizeSpan; import android.util.TypedValue; import android.view.Gravity; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.TextView; @@ -51,6 +52,7 @@ public class TextInfoPrivacyCell extends FrameLayout { textView.setMovementMethod(LinkMovementMethod.getInstance()); textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText4)); textView.setLinkTextColor(Theme.getColor(linkTextColorKey)); + textView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, padding, 0, padding, 0)); } @@ -126,4 +128,11 @@ public class TextInfoPrivacyCell extends FrameLayout { textView.setAlpha(value ? 1.0f : 0.5f); } } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(TextView.class.getName()); + info.setText(text); + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ThemesHorizontalListCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ThemesHorizontalListCell.java index b2c692a9c..f451ed335 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ThemesHorizontalListCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ThemesHorizontalListCell.java @@ -20,6 +20,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.net.Uri; +import android.os.Build; import android.os.SystemClock; import android.text.TextPaint; import android.text.TextUtils; @@ -28,6 +29,8 @@ import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; import android.widget.FrameLayout; import org.telegram.messenger.AndroidUtilities; @@ -515,10 +518,7 @@ public class ThemesHorizontalListCell extends RecyclerListView implements Notifi int y = AndroidUtilities.dp(11); rect.set(x, y, x + AndroidUtilities.dp(76), y + AndroidUtilities.dp(97)); - String name = themeInfo.getName(); - if (name.toLowerCase().endsWith(".attheme")) { - name = name.substring(0, name.lastIndexOf('.')); - } + String name = getThemeName(); int maxWidth = getMeasuredWidth() - AndroidUtilities.dp(isFirst ? 10 : 15) - (isLast ? AndroidUtilities.dp(7) : 0); String text = TextUtils.ellipsize(name, textPaint, maxWidth, TextUtils.TruncateAt.END).toString(); int width = (int) Math.ceil(textPaint.measureText(text)); @@ -637,6 +637,14 @@ public class ThemesHorizontalListCell extends RecyclerListView implements Notifi } } + private String getThemeName() { + String name = themeInfo.getName(); + if (name.toLowerCase().endsWith(".attheme")) { + name = name.substring(0, name.lastIndexOf('.')); + } + return name; + } + private int blend(int color1, int color2) { if (accentState == 1.0f) { return color2; @@ -644,6 +652,20 @@ public class ThemesHorizontalListCell extends RecyclerListView implements Notifi return (int) evaluator.evaluate(accentState, color1, color2); } } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setText(getThemeName()); + info.setClassName(Button.class.getName()); + info.setChecked(button.isChecked()); + info.setCheckable(true); + info.setEnabled(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_LONG_CLICK, LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions))); + } + } } public ThemesHorizontalListCell(Context context, int type, ArrayList def, ArrayList dark) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChangeBioActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChangeBioActivity.java index 010ac9717..3f45ebe98 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChangeBioActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChangeBioActivity.java @@ -74,6 +74,7 @@ public class ChangeBioActivity extends BaseFragment { ActionBarMenu menu = actionBar.createMenu(); doneButton = menu.addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56)); + doneButton.setContentDescription(LocaleController.getString("Done", R.string.Done)); fragmentView = new LinearLayout(context); LinearLayout linearLayout = (LinearLayout) fragmentView; @@ -146,9 +147,11 @@ public class ChangeBioActivity extends BaseFragment { checkTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); checkTextView.setText(String.format("%d", 70)); checkTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText4)); + checkTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); fieldContainer.addView(checkTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT, 0, 4, 4, 0)); helpTextView = new TextView(context); + helpTextView.setFocusable(true); helpTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); helpTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText8)); helpTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelAdminLogActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelAdminLogActivity.java index 40fa9b059..a5dcf21b8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelAdminLogActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelAdminLogActivity.java @@ -48,6 +48,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.DatePicker; import android.widget.EditText; import android.widget.FrameLayout; @@ -852,6 +853,7 @@ public class ChannelAdminLogActivity extends BaseFragment implements Notificatio floatingDateView = new ChatActionCell(context); floatingDateView.setAlpha(0.0f); + floatingDateView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); contentView.addView(floatingDateView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 4, 0, 0)); contentView.addView(actionBar); @@ -1923,7 +1925,7 @@ public class ChannelAdminLogActivity extends BaseFragment implements Notificatio MediaController.getInstance().setVoiceMessagesPlaylist(null, false); return result; } else if (messageObject.isMusic()) { - return MediaController.getInstance().setPlaylist(messages, messageObject); + return MediaController.getInstance().setPlaylist(messages, messageObject, 0); } return false; } @@ -2023,7 +2025,7 @@ public class ChannelAdminLogActivity extends BaseFragment implements Notificatio if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && messageObject.messageOwner.media.webpage != null && messageObject.messageOwner.media.webpage.cached_page != null) { String lowerUrl = urlFinal.toLowerCase(); String lowerUrl2 = messageObject.messageOwner.media.webpage.url.toLowerCase(); - if ((lowerUrl.contains("telegra.ph") || lowerUrl.contains("t.me/iv")) && (lowerUrl.contains(lowerUrl2) || lowerUrl2.contains(lowerUrl))) { + if ((Browser.isTelegraphUrl(lowerUrl, false) || lowerUrl.contains("t.me/iv")) && (lowerUrl.contains(lowerUrl2) || lowerUrl2.contains(lowerUrl))) { ArticleViewer.getInstance().setParentActivity(getParentActivity(), ChannelAdminLogActivity.this); ArticleViewer.getInstance().open(messageObject); return; @@ -2039,7 +2041,7 @@ public class ChannelAdminLogActivity extends BaseFragment implements Notificatio @Override public void needOpenWebView(String url, String title, String description, String originalUrl, int w, int h) { - EmbedBottomSheet.show(mContext, title, description, originalUrl, url, w, h); + EmbedBottomSheet.show(mContext, title, description, originalUrl, url, w, h, false); } @Override @@ -2151,7 +2153,15 @@ public class ChannelAdminLogActivity extends BaseFragment implements Notificatio }); chatMessageCell.setAllowAssistant(true); } else if (viewType == 1) { - view = new ChatActionCell(mContext); + view = new ChatActionCell(mContext) { + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + // if alpha == 0, then visibleToUser == false, so we need to override it + // to keep accessibility working correctly + info.setVisibleToUser(true); + } + }; ((ChatActionCell) view).setDelegate(new ChatActionCell.ChatActionCellDelegate() { @Override public void didClickImage(ChatActionCell cell) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java index b2a8f2cd4..ef158cff4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java @@ -12,6 +12,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; @@ -123,7 +124,10 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC private int currentStep; private int chatId; private boolean canCreatePublic = true; - private TLRPC.InputFile uploadedAvatar; + private TLRPC.InputFile inputPhoto; + private TLRPC.InputFile inputVideo; + private String inputVideoPath; + private double videoTimestamp; private boolean createAfterUpload; private boolean donePressed; @@ -135,7 +139,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC currentStep = args.getInt("step", 0); if (currentStep == 0) { avatarDrawable = new AvatarDrawable(); - imageUpdater = new ImageUpdater(); + imageUpdater = new ImageUpdater(true); TLRPC.TL_channels_checkUsername req = new TLRPC.TL_channels_checkUsername(); req.username = "1"; @@ -162,7 +166,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC } if (imageUpdater != null) { imageUpdater.parentFragment = this; - imageUpdater.delegate = this; + imageUpdater.setDelegate(this); } return super.onFragmentCreate(); } @@ -188,6 +192,9 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC nameTextView.onResume(); } AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); + if (imageUpdater != null) { + imageUpdater.onResume(); + } } @Override @@ -196,6 +203,29 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC if (nameTextView != null) { nameTextView.onPause(); } + if (imageUpdater != null) { + imageUpdater.onPause(); + } + } + + @Override + public void dismissCurrentDialog() { + if (imageUpdater != null && imageUpdater.dismissCurrentDialog(visibleDialog)) { + return; + } + super.dismissCurrentDialog(); + } + + @Override + public boolean dismissDialogOnPause(Dialog dialog) { + return (imageUpdater == null || imageUpdater.dismissDialogOnPause(dialog)) && super.dismissDialogOnPause(dialog); + } + + @Override + public void onRequestPermissionsResultFragment(int requestCode, String[] permissions, int[] grantResults) { + if (imageUpdater != null) { + imageUpdater.onRequestPermissionsResultFragment(requestCode, permissions, grantResults); + } } @Override @@ -232,7 +262,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC return; } donePressed = true; - if (imageUpdater.uploadingImage != null) { + if (imageUpdater.isUploadingImage()) { createAfterUpload = true; progressDialog = new AlertDialog(getParentActivity(), 3); progressDialog.setOnCancelListener(dialog -> { @@ -446,7 +476,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC protected void onDraw(Canvas canvas) { if (avatarImage != null && avatarImage.getImageReceiver().hasNotThumb()) { paint.setAlpha((int) (0x55 * avatarImage.getImageReceiver().getCurrentAlpha())); - canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, AndroidUtilities.dp(32), paint); + canvas.drawCircle(getMeasuredWidth() / 2.0f, getMeasuredHeight() / 2.0f, getMeasuredWidth() / 2.0f, paint); } } }; @@ -454,7 +484,10 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC avatarOverlay.setOnClickListener(view -> imageUpdater.openMenu(avatar != null, () -> { avatar = null; avatarBig = null; - uploadedAvatar = null; + inputPhoto = null; + inputVideo = null; + inputVideoPath = null; + videoTimestamp = 0; showAvatarProgress(false, true); avatarImage.setImage(null, null, avatarDrawable, null); })); @@ -483,6 +516,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC avatarProgressView = new RadialProgressView(context); avatarProgressView.setSize(AndroidUtilities.dp(30)); avatarProgressView.setProgressColor(0xffffffff); + avatarProgressView.setNoProgress(false); frameLayout.addView(avatarProgressView, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 12)); showAvatarProgress(false, false); @@ -761,10 +795,29 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC } @Override - public void didUploadPhoto(final TLRPC.InputFile file, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { + public void onUploadProgressChanged(float progress) { + if (avatarProgressView == null) { + return; + } + avatarProgressView.setProgress(progress); + } + + @Override + public void didStartUpload(boolean isVideo) { + if (avatarProgressView == null) { + return; + } + avatarProgressView.setProgress(0.0f); + } + + @Override + public void didUploadPhoto(final TLRPC.InputFile photo, final TLRPC.InputFile video, double videoStartTimestamp, String videoPath, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { AndroidUtilities.runOnUIThread(() -> { - if (file != null) { - uploadedAvatar = file; + if (photo != null || video != null) { + inputPhoto = photo; + inputVideo = video; + inputVideoPath = videoPath; + videoTimestamp = videoStartTimestamp; if (createAfterUpload) { try { if (progressDialog != null && progressDialog.isShowing()) { @@ -920,8 +973,8 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC bundle.putInt("step", 1); bundle.putInt("chat_id", chat_id); bundle.putBoolean("canCreatePublic", canCreatePublic); - if (uploadedAvatar != null) { - MessagesController.getInstance(currentAccount).changeChatAvatar(chat_id, uploadedAvatar, avatar, avatarBig); + if (inputPhoto != null || inputVideo != null) { + MessagesController.getInstance(currentAccount).changeChatAvatar(chat_id, null, inputPhoto, inputVideo, videoTimestamp, inputVideoPath, avatar, avatarBig); } presentFragment(new ChannelCreateActivity(bundle), true); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/BaseChartView.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/BaseChartView.java index c3bc1c235..203a16705 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/BaseChartView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/BaseChartView.java @@ -46,7 +46,7 @@ public abstract class BaseChartView public ArrayList lines = new ArrayList<>(); private final int ANIM_DURATION = 400; - public final static int HORIZONTAL_PADDING = AndroidUtilities.dp(16f); + public final static float HORIZONTAL_PADDING = AndroidUtilities.dpf2(16f); private final static float LINE_WIDTH = 1; private final static float SELECTED_LINE_WIDTH = AndroidUtilities.dpf2(1.5f); private final static float SIGNATURE_TEXT_SIZE = AndroidUtilities.dpf2(12f); @@ -155,12 +155,12 @@ public abstract class BaseChartView private final int touchSlop; public int pikerHeight = AndroidUtilities.dp(46); - public int pickerWidth; - public int chartStart; - public int chartEnd; - public int chartWidth; + public float pickerWidth; + public float chartStart; + public float chartEnd; + public float chartWidth; public float chartFullWidth; - public Rect chartArea = new Rect(); + public RectF chartArea = new RectF(); private ValueAnimator.AnimatorUpdateListener pickerHeightUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override @@ -312,10 +312,10 @@ public abstract class BaseChartView if (getMeasuredWidth() != lastW || getMeasuredHeight() != lastH) { lastW = getMeasuredWidth(); lastH = getMeasuredHeight(); - bottomChartBitmap = Bitmap.createBitmap(getMeasuredWidth() - (HORIZONTAL_PADDING << 1), pikerHeight, Bitmap.Config.ARGB_4444); + bottomChartBitmap = Bitmap.createBitmap((int) (getMeasuredWidth() - (HORIZONTAL_PADDING * 2f)), pikerHeight, Bitmap.Config.ARGB_4444); bottomChartCanvas = new Canvas(bottomChartBitmap); - sharedUiComponents.getPickerMaskBitmap(pikerHeight, getMeasuredWidth() - HORIZONTAL_PADDING * 2); + sharedUiComponents.getPickerMaskBitmap(pikerHeight, (int) (getMeasuredWidth() - HORIZONTAL_PADDING * 2)); measureSizes(); if (legendShowing) @@ -705,7 +705,7 @@ public abstract class BaseChartView } canvas.drawBitmap( - sharedUiComponents.getPickerMaskBitmap(pikerHeight, getMeasuredWidth() - HORIZONTAL_PADDING * 2), + sharedUiComponents.getPickerMaskBitmap(pikerHeight, (int) (getMeasuredWidth() - HORIZONTAL_PADDING * 2)), HORIZONTAL_PADDING, getMeasuredHeight() - PICKER_PADDING - pikerHeight, emptyPaint); if (chartData != null) { @@ -1093,7 +1093,7 @@ public abstract class BaseChartView MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST) ); float lXPoint = chartData.xPercentage[selectedIndex] * chartFullWidth - offset; - if (lXPoint > (chartStart + chartWidth) >> 1) { + if (lXPoint > (chartStart + chartWidth) / 2f) { lXPoint -= (legendSignatureView.getWidth() + DP_5); } else { lXPoint += DP_5; @@ -1265,6 +1265,9 @@ public abstract class BaseChartView endXIndex = chartData.findEndIndex(startXIndex, Math.min( pickerDelegate.pickerEnd, 1f )); + if (endXIndex < startXIndex) { + endXIndex = startXIndex; + } if (chartHeaderView != null) { chartHeaderView.setDates(chartData.x[startXIndex], chartData.x[endXIndex]); } @@ -1593,4 +1596,8 @@ public abstract class BaseChartView invalidate = true; } } + + public void fillTransitionParams(TransitionParams params) { + + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/ChartPickerDelegate.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/ChartPickerDelegate.java index 00709e242..aeae132b7 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/ChartPickerDelegate.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/ChartPickerDelegate.java @@ -20,7 +20,7 @@ public class ChartPickerDelegate { private final static int CAPTURE_LEFT = 1; private final static int CAPTURE_RIGHT = 1 << 1; private final static int CAPTURE_MIDDLE = 1 << 2; - public int pickerWidth; + public float pickerWidth; public boolean tryMoveTo; public float moveToX; public float moveToY; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/PieChartView.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/PieChartView.java index 96b1ee1a6..1f47e8e4f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/PieChartView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/PieChartView.java @@ -18,6 +18,7 @@ import org.telegram.ui.Charts.view_data.ChartHorizontalLinesData; import org.telegram.ui.Charts.view_data.LegendSignatureView; import org.telegram.ui.Charts.view_data.LineViewData; import org.telegram.ui.Charts.view_data.PieLegendView; +import org.telegram.ui.Charts.view_data.TransitionParams; import tw.nekomimi.nekogram.NekoConfig; @@ -64,13 +65,11 @@ public class PieChartView extends StackLinearChartView { int transitionAlpha = 255; - canvas.save(); + if (canvas != null) { + canvas.save(); + } if (transitionMode == TRANSITION_MODE_CHILD) { transitionAlpha = (int) (transitionParams.progress * transitionParams.progress * 255); - canvas.scale(transitionParams.progress, transitionParams.progress, - chartArea.centerX(), - chartArea.centerY() - ); } if (isEmpty) { @@ -93,10 +92,12 @@ public class PieChartView extends StackLinearChartView { transitionAlpha = (int) (transitionAlpha * emptyDataAlpha); float sc = 0.4f + emptyDataAlpha * 0.6f; - canvas.scale(sc, sc, - chartArea.centerX(), - chartArea.centerY() - ); + if (canvas != null) { + canvas.scale(sc, sc, + chartArea.centerX(), + chartArea.centerY() + ); + } int radius = (int) ((chartArea.width() > chartArea.height() ? chartArea.height() : chartArea.width()) * 0.45f); rectF.set( @@ -118,7 +119,9 @@ public class PieChartView extends StackLinearChartView { localSum += v; } if (localSum == 0) { - canvas.restore(); + if (canvas != null) { + canvas.restore(); + } return; } for (int i = 0; i < n; i++) { @@ -132,72 +135,78 @@ public class PieChartView extends StackLinearChartView { continue; } - canvas.save(); + if (canvas != null) { + canvas.save(); + } double textAngle = a + (currentPercent / 2f) * 360f; if (lines.get(i).selectionA > 0f) { float ai = INTERPOLATOR.getInterpolation(lines.get(i).selectionA); - canvas.translate( - (float) (Math.cos(Math.toRadians(textAngle)) * AndroidUtilities.dp(8) * ai), - (float) (Math.sin(Math.toRadians(textAngle)) * AndroidUtilities.dp(8) * ai) - ); + if (canvas != null) { + canvas.translate( + (float) (Math.cos(Math.toRadians(textAngle)) * AndroidUtilities.dp(8) * ai), + (float) (Math.sin(Math.toRadians(textAngle)) * AndroidUtilities.dp(8) * ai) + ); + } } lines.get(i).paint.setStyle(Paint.Style.FILL_AND_STROKE); lines.get(i).paint.setStrokeWidth(1); lines.get(i).paint.setAntiAlias(!USE_LINES); - canvas.drawArc( - rectF, - a, - (currentPercent) * 360f, - true, - lines.get(i).paint); - - lines.get(i).paint.setStyle(Paint.Style.STROKE); - - canvas.restore(); + if (canvas != null && transitionMode != TRANSITION_MODE_CHILD) { + canvas.drawArc( + rectF, + a, + (currentPercent) * 360f, + true, + lines.get(i).paint); + lines.get(i).paint.setStyle(Paint.Style.STROKE); + canvas.restore(); + } lines.get(i).paint.setAlpha(255); a += currentPercent * 360f; } a = -90f; - for (int i = 0; i < n; i++) { - if (lines.get(i).alpha <= 0 && !lines.get(i).enabled) continue; - float currentPercent = (lines.get(i).drawingPart * lines.get(i).alpha / localSum); - canvas.save(); + if (canvas != null) { + for (int i = 0; i < n; i++) { + if (lines.get(i).alpha <= 0 && !lines.get(i).enabled) continue; + float currentPercent = (lines.get(i).drawingPart * lines.get(i).alpha / localSum); - double textAngle = a + (currentPercent / 2f) * 360f; + canvas.save(); + double textAngle = a + (currentPercent / 2f) * 360f; - if (lines.get(i).selectionA > 0f) { - float ai = INTERPOLATOR.getInterpolation(lines.get(i).selectionA); - canvas.translate( - (float) (Math.cos(Math.toRadians(textAngle)) * AndroidUtilities.dp(8) * ai), - (float) (Math.sin(Math.toRadians(textAngle)) * AndroidUtilities.dp(8) * ai) - ); - } + if (lines.get(i).selectionA > 0f) { + float ai = INTERPOLATOR.getInterpolation(lines.get(i).selectionA); + canvas.translate( + (float) (Math.cos(Math.toRadians(textAngle)) * AndroidUtilities.dp(8) * ai), + (float) (Math.sin(Math.toRadians(textAngle)) * AndroidUtilities.dp(8) * ai) + ); + } - int percent = (int) (100f * currentPercent); - if (currentPercent >= 0.02f && percent > 0 && percent <= 100) { - rText = (float) (rectF.width() * 0.42f * Math.sqrt(1f - currentPercent)); - textPaint.setTextSize(MIN_TEXT_SIZE + currentPercent * MAX_TEXT_SIZE); - textPaint.setAlpha((int) (transitionAlpha * lines.get(i).alpha)); - canvas.drawText( - lookupTable[percent], - (float) (rectF.centerX() + rText * Math.cos(Math.toRadians(textAngle))), - (float) (rectF.centerY() + rText * Math.sin(Math.toRadians(textAngle))) - ((textPaint.descent() + textPaint.ascent()) / 2), - textPaint); + int percent = (int) (100f * currentPercent); + if (currentPercent >= 0.02f && percent > 0 && percent <= 100) { + rText = (float) (rectF.width() * 0.42f * Math.sqrt(1f - currentPercent)); + textPaint.setTextSize(MIN_TEXT_SIZE + currentPercent * MAX_TEXT_SIZE); + textPaint.setAlpha((int) (transitionAlpha * lines.get(i).alpha)); + canvas.drawText( + lookupTable[percent], + (float) (rectF.centerX() + rText * Math.cos(Math.toRadians(textAngle))), + (float) (rectF.centerY() + rText * Math.sin(Math.toRadians(textAngle))) - ((textPaint.descent() + textPaint.ascent()) / 2), + textPaint); + } + + canvas.restore(); + + lines.get(i).paint.setAlpha(255); + a += currentPercent * 360f; } canvas.restore(); - - lines.get(i).paint.setAlpha(255); - a += currentPercent * 360f; } - - canvas.restore(); } @Override @@ -352,6 +361,7 @@ public class PieChartView extends StackLinearChartView { LineViewData l = lines.get(newSelection); pieLegendView.setData(l.line.name, (int) values[currentSelection], l.lineColor); + pieLegendView.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.AT_MOST)); float r = rectF.width() / 2; int xl = (int) Math.min( @@ -360,6 +370,9 @@ public class PieChartView extends StackLinearChartView { ); if (xl < 0) xl = 0; + if (xl + pieLegendView.getMeasuredWidth() > getMeasuredWidth() - AndroidUtilities.dp((16))) { + xl -= xl + pieLegendView.getMeasuredWidth() - (getMeasuredWidth() - AndroidUtilities.dp(16)); + } int yl = (int) Math.min( (rectF.centerY() + r * Math.sin(Math.toRadians((selectionStartA * 360f) - 90f))), @@ -567,4 +580,14 @@ public class PieChartView extends StackLinearChartView { invalidate(); } } + + @Override + public void fillTransitionParams(TransitionParams params) { + drawChart(null); + float p = 0; + for (int i = 0; i < darawingValuesPercentage.length; i++) { + p += darawingValuesPercentage[i]; + params.angle[i] = p * 360 - 180; + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/StackLinearChartView.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/StackLinearChartView.java index 9c76eb4c7..e3e90bd45 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/StackLinearChartView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/StackLinearChartView.java @@ -2,16 +2,22 @@ package org.telegram.ui.Charts; import android.content.Context; import android.graphics.Canvas; +import android.graphics.Matrix; import android.graphics.Path; import android.graphics.RectF; +import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Charts.data.ChartData; import org.telegram.ui.Charts.data.StackLinearChartData; import org.telegram.ui.Charts.view_data.LineViewData; import org.telegram.ui.Charts.view_data.StackLinearViewData; +import org.telegram.ui.Charts.view_data.TransitionParams; public class StackLinearChartView extends BaseChartView { + private Matrix matrix = new Matrix(); + private float[] mapPoints = new float[2]; public StackLinearChartView(Context context) { super(context); @@ -27,6 +33,7 @@ public class StackLinearChartView extends BaseCha Path ovalPath = new Path(); boolean[] skipPoints; + float startFromY[]; @Override protected void drawChart(Canvas canvas) { @@ -34,6 +41,9 @@ public class StackLinearChartView extends BaseCha float fullWidth = (chartWidth / (pickerDelegate.pickerEnd - pickerDelegate.pickerStart)); float offset = fullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING; + float cX = chartArea.centerX(); + float cY = chartArea.centerY() + AndroidUtilities.dp(16); + for (int k = 0; k < lines.size(); k++) { lines.get(k).chartPath.reset(); lines.get(k).chartPathPicker.reset(); @@ -42,25 +52,30 @@ public class StackLinearChartView extends BaseCha canvas.save(); if (skipPoints == null || skipPoints.length < chartData.lines.size()) { skipPoints = new boolean[chartData.lines.size()]; + startFromY = new float[chartData.lines.size()]; } + boolean hasEmptyPoint = false; int transitionAlpha = 255; + float transitionProgressHalf = 0; if (transitionMode == TRANSITION_MODE_PARENT) { - - transitionAlpha = (int) ((1f - transitionParams.progress) * 255); + transitionProgressHalf = transitionParams.progress / 0.6f; + if (transitionProgressHalf > 1f) { + transitionProgressHalf = 1f; + } + // transitionAlpha = (int) ((1f - transitionParams.progress) * 255); ovalPath.reset(); - int radiusStart = (chartArea.width() > chartArea.height() ? chartArea.width() : chartArea.height()); - int radiusEnd = (int) ((chartArea.width() > chartArea.height() ? chartArea.height() : chartArea.width()) / 2f); + float radiusStart = (chartArea.width() > chartArea.height() ? chartArea.width() : chartArea.height()); + float radiusEnd = (chartArea.width() > chartArea.height() ? chartArea.height() : chartArea.width()) * 0.45f; float radius = radiusEnd + ((radiusStart - radiusEnd) / 2) * (1 - transitionParams.progress); - radius *= 1f - transitionParams.progress; RectF rectF = new RectF(); rectF.set( - chartArea.centerX() - radius, - chartArea.centerY() - radius, - chartArea.centerX() + radius, - chartArea.centerY() + radius + cX - radius, + cY - radius, + cX + radius, + cY + radius ); ovalPath.addRoundRect( rectF, radius, radius, Path.Direction.CW @@ -70,6 +85,11 @@ public class StackLinearChartView extends BaseCha transitionAlpha = (int) (transitionParams.progress * 255); } + float dX = 0; + float dY = 0; + float x1 = 0; + float y1 = 0; + float p; if (chartData.xPercentage.length < 2) { p = 1f; @@ -83,10 +103,12 @@ public class StackLinearChartView extends BaseCha float startXPoint = 0; float endXPoint = 0; + + for (int i = localStart; i <= localEnd; i++) { float stackOffset = 0; float sum = 0; - float xPoint = chartData.xPercentage[i] * fullWidth - offset; + int lastEnabled = 0; int drawingLinesCount = 0; for (int k = 0; k < lines.size(); k++) { @@ -96,6 +118,7 @@ public class StackLinearChartView extends BaseCha sum += line.line.y[i] * line.alpha; drawingLinesCount++; } + lastEnabled = k; } for (int k = 0; k < lines.size(); k++) { @@ -119,54 +142,255 @@ public class StackLinearChartView extends BaseCha } } + float xPoint = chartData.xPercentage[i] * fullWidth - offset; + float nextXPoint; + if (i == localEnd) { + nextXPoint = getMeasuredWidth(); + } else { + nextXPoint = chartData.xPercentage[i + 1] * fullWidth - offset; + } + if (yPercentage == 0 && k == lastEnabled) { + hasEmptyPoint = true; + } float height = (yPercentage) * (getMeasuredHeight() - chartBottom - SIGNATURE_TEXT_HEIGHT); float yPoint = getMeasuredHeight() - chartBottom - height - stackOffset; + startFromY[k] = yPoint; + + float angle = 0; + float yPointZero = getMeasuredHeight() - chartBottom; + float xPointZero = xPoint; + if (i == localEnd) { + endXPoint = xPoint; + } else if (i == localStart) { + startXPoint = xPoint; + } + if (transitionMode == TRANSITION_MODE_PARENT && k != lastEnabled) { + if (xPoint < cX) { + x1 = transitionParams.startX[k]; + y1 = transitionParams.startY[k]; + } else { + x1 = transitionParams.endX[k]; + y1 = transitionParams.endY[k]; + } + + dX = cX - x1; + dY = cY - y1; + float yTo = dY * (xPoint - x1) / dX + y1; + + yPoint = yPoint * (1f - transitionProgressHalf) + yTo * transitionProgressHalf; + yPointZero = yPointZero * (1f - transitionProgressHalf) + yTo * transitionProgressHalf; + + float angleK = dY / dX; + if (angleK > 0) { + angle = (float) Math.toDegrees(-Math.atan(angleK)); + } else { + angle = (float) Math.toDegrees(Math.atan(Math.abs(angleK))); + } + angle -= 90; + + if (xPoint >= cX) { + mapPoints[0] = xPoint; + mapPoints[1] = yPoint; + matrix.reset(); + matrix.postRotate(transitionParams.progress * angle, cX, cY); + matrix.mapPoints(mapPoints); + + xPoint = mapPoints[0]; + yPoint = mapPoints[1]; + if (xPoint < cX) xPoint = cX; + + mapPoints[0] = xPointZero; + mapPoints[1] = yPointZero; + matrix.reset(); + matrix.postRotate(transitionParams.progress * angle, cX, cY); + matrix.mapPoints(mapPoints); + yPointZero = mapPoints[1]; + if (xPointZero < cX) xPointZero = cX; + } else { + if (nextXPoint >= cX) { + xPointZero = xPoint = xPoint * (1f - transitionProgressHalf) + cX * transitionProgressHalf; + yPointZero = yPoint = yPoint * (1f - transitionProgressHalf) + cY * transitionProgressHalf; + } else { + mapPoints[0] = xPoint; + mapPoints[1] = yPoint; + matrix.reset(); + matrix.postRotate(transitionParams.progress * angle + transitionParams.progress * transitionParams.angle[k], cX, cY); + matrix.mapPoints(mapPoints); + xPoint = mapPoints[0]; + yPoint = mapPoints[1]; + + if (nextXPoint >= cX) { + mapPoints[0] = xPointZero * (1f - transitionParams.progress) + cX * transitionParams.progress; + } else { + mapPoints[0] = xPointZero; + } + mapPoints[1] = yPointZero; + matrix.reset(); + matrix.postRotate(transitionParams.progress * angle + transitionParams.progress * transitionParams.angle[k], cX, cY); + matrix.mapPoints(mapPoints); + + xPointZero = mapPoints[0]; + yPointZero = mapPoints[1]; + } + } + } if (i == localStart) { - line.chartPath.moveTo(0, getMeasuredHeight()); - startXPoint = xPoint; + float localX = 0; + float localY = getMeasuredHeight(); + if (transitionMode == TRANSITION_MODE_PARENT && k != lastEnabled) { + mapPoints[0] = localX - cX; + mapPoints[1] = localY; + matrix.reset(); + matrix.postRotate(transitionParams.progress * angle + transitionParams.progress * transitionParams.angle[k], cX, cY); + matrix.mapPoints(mapPoints); + localX = mapPoints[0]; + localY = mapPoints[1]; + } + line.chartPath.moveTo(localX, localY); skipPoints[k] = false; } - if (yPercentage == 0 && (i > 0 && y[i - 1] == 0) && (i < localEnd && y[i + 1] == 0)) { + float transitionProgress = transitionParams == null ? 0f : transitionParams.progress; + if (yPercentage == 0 && (i > 0 && y[i - 1] == 0) && (i < localEnd && y[i + 1] == 0) && transitionMode != TRANSITION_MODE_PARENT) { if (!skipPoints[k]) { - line.chartPath.lineTo(xPoint, getMeasuredHeight() - chartBottom); + if (k == lastEnabled) { + line.chartPath.lineTo(xPointZero, yPointZero * (1f - transitionProgress)); + } else { + line.chartPath.lineTo(xPointZero, yPointZero); + } } skipPoints[k] = true; } else { if (skipPoints[k]) { - line.chartPath.lineTo(xPoint, getMeasuredHeight() - chartBottom); + if (k == lastEnabled) { + line.chartPath.lineTo(xPointZero, yPointZero * (1f - transitionProgress)); + } else { + line.chartPath.lineTo(xPointZero, yPointZero); + } + } + if (k == lastEnabled) { + line.chartPath.lineTo(xPoint, yPoint * (1f - transitionProgress)); + } else { + line.chartPath.lineTo(xPoint, yPoint); } - line.chartPath.lineTo(xPoint, yPoint); skipPoints[k] = false; } if (i == localEnd) { - line.chartPath.lineTo(getMeasuredWidth(), getMeasuredHeight()); - endXPoint = xPoint; + float localX = getMeasuredWidth(); + float localY = getMeasuredHeight(); + if (transitionMode == TRANSITION_MODE_PARENT && k != lastEnabled) { + mapPoints[0] = localX + cX; + mapPoints[1] = localY; + matrix.reset(); + matrix.postRotate(transitionParams.progress * transitionParams.angle[k], cX, cY); + matrix.mapPoints(mapPoints); + localX = mapPoints[0]; + localY = mapPoints[1]; + } else { + line.chartPath.lineTo(localX, localY); + } + + if (transitionMode == TRANSITION_MODE_PARENT && k != lastEnabled) { + + x1 = transitionParams.startX[k]; + y1 = transitionParams.startY[k]; + + dX = cX - x1; + dY = cY - y1; + float angleK = dY / dX; + if (angleK > 0) { + angle = (float) Math.toDegrees(-Math.atan(angleK)); + } else { + angle = (float) Math.toDegrees(Math.atan(Math.abs(angleK))); + } + angle -= 90; + + localX = transitionParams.startX[k]; + localY = transitionParams.startY[k]; + mapPoints[0] = localX; + mapPoints[1] = localY; + matrix.reset(); + matrix.postRotate(transitionParams.progress * angle + transitionParams.progress * transitionParams.angle[k], cX, cY); + matrix.mapPoints(mapPoints); + localX = mapPoints[0]; + localY = mapPoints[1]; + + // 0 right_top + // 1 right_bottom + // 2 left_bottom + // 3 left_top + int endQuarter; + int startQuarter; + + if (Math.abs(xPoint - localX) < 0.001 && ((localY < cY && yPoint < cY) || (localY > cY && yPoint > cY))) { + if (transitionParams.angle[k] == -180f) { + endQuarter = 0; + startQuarter = 0; + } else { + endQuarter = 0; + startQuarter = 3; + } + } else { + endQuarter = quarterForPoint(xPoint, yPoint); + startQuarter = quarterForPoint(localX, localY); + } + + for (int q = endQuarter; q <= startQuarter; q++) { + if (q == 0) { + line.chartPath.lineTo(getMeasuredWidth(), 0); + } else if (q == 1) { + line.chartPath.lineTo(getMeasuredWidth(), getMeasuredHeight()); + } else if (q == 2) { + line.chartPath.lineTo(0, getMeasuredHeight()); + } else { + line.chartPath.lineTo(0, 0); + } + } + } } stackOffset += height; } } - canvas.save(); + canvas.clipRect(startXPoint, SIGNATURE_TEXT_HEIGHT, endXPoint, getMeasuredHeight() - chartBottom); + + if (hasEmptyPoint) { + canvas.drawColor(Theme.getColor(Theme.key_statisticChartLineEmpty)); + } for (int k = lines.size() - 1; k >= 0; k--) { LineViewData line = lines.get(k); line.paint.setAlpha(transitionAlpha); - canvas.drawPath(line.chartPath, line.paint); - line.paint.setAlpha(255); } + canvas.restore(); canvas.restore(); } } + private int quarterForPoint(float x, float y) { + float cX = chartArea.centerX(); + float cY = chartArea.centerY() + AndroidUtilities.dp(16); + + if (x >= cX && y <= cY) { + return 0; + } + if (x >= cX && y >= cY) { + return 1; + } + if (x < cX && y >= cY) { + return 2; + } + return 3; + } + @Override protected void drawPickerChart(Canvas canvas) { if (chartData != null) { @@ -181,10 +405,12 @@ public class StackLinearChartView extends BaseCha skipPoints = new boolean[chartData.lines.size()]; } + boolean hasEmptyPoint = false; + for (int i = 0; i < n; i++) { float stackOffset = 0; float sum = 0; - + int lastEnabled = 0; int drawingLinesCount = 0; for (int k = 0; k < lines.size(); k++) { @@ -194,6 +420,7 @@ public class StackLinearChartView extends BaseCha sum += chartData.simplifiedY[k][i] * line.alpha; drawingLinesCount++; } + lastEnabled = k; } float xPoint = i / (float) (n - 1) * pickerWidth; @@ -217,6 +444,10 @@ public class StackLinearChartView extends BaseCha } } + if (yPercentage == 0 && k == lastEnabled) { + hasEmptyPoint = true; + } + float height = (yPercentage) * (pikerHeight); float yPoint = pikerHeight - height - stackOffset; @@ -248,6 +479,9 @@ public class StackLinearChartView extends BaseCha } } + if (hasEmptyPoint) { + canvas.drawColor(Theme.getColor(Theme.key_statisticChartLineEmpty)); + } for (int k = lines.size() - 1; k >= 0; k--) { LineViewData line = lines.get(k); canvas.drawPath(line.chartPathPicker, line.paint); @@ -281,4 +515,86 @@ public class StackLinearChartView extends BaseCha return 0.1f; } + @Override + public void fillTransitionParams(TransitionParams params) { + if (chartData == null) { + return; + } + float fullWidth = (chartWidth / (pickerDelegate.pickerEnd - pickerDelegate.pickerStart)); + float offset = fullWidth * (pickerDelegate.pickerStart) - HORIZONTAL_PADDING; + + float p; + if (chartData.xPercentage.length < 2) { + p = 1f; + } else { + p = chartData.xPercentage[1] * fullWidth; + } + + int additionalPoints = (int) (HORIZONTAL_PADDING / p) + 1; + int localStart = Math.max(0, startXIndex - additionalPoints - 1); + int localEnd = Math.min(chartData.xPercentage.length - 1, endXIndex + additionalPoints + 1); + + + transitionParams.startX = new float[chartData.lines.size()]; + transitionParams.startY = new float[chartData.lines.size()]; + transitionParams.endX = new float[chartData.lines.size()]; + transitionParams.endY = new float[chartData.lines.size()]; + transitionParams.angle = new float[chartData.lines.size()]; + + + for (int j = 0; j < 2; j++) { + int i = localStart; + if (j == 1) { + i = localEnd; + } + int stackOffset = 0; + float sum = 0; + int drawingLinesCount = 0; + for (int k = 0; k < lines.size(); k++) { + LineViewData line = lines.get(k); + if (!line.enabled && line.alpha == 0) continue; + if (line.line.y[i] > 0) { + sum += line.line.y[i] * line.alpha; + drawingLinesCount++; + } + } + + for (int k = 0; k < lines.size(); k++) { + LineViewData line = lines.get(k); + if (!line.enabled && line.alpha == 0) continue; + int[] y = line.line.y; + + float yPercentage; + + if (drawingLinesCount == 1) { + if (y[i] == 0) { + yPercentage = 0; + } else { + yPercentage = line.alpha; + } + } else { + if (sum == 0) { + yPercentage = 0; + } else { + yPercentage = y[i] * line.alpha / sum; + } + } + + float xPoint = chartData.xPercentage[i] * fullWidth - offset; + float height = (yPercentage) * (getMeasuredHeight() - chartBottom - SIGNATURE_TEXT_HEIGHT); + float yPoint = getMeasuredHeight() - chartBottom - height - stackOffset; + stackOffset += height; + + if (j == 0) { + transitionParams.startX[k] = xPoint; + transitionParams.startY[k] = yPoint; + } else { + transitionParams.endX[k] = xPoint; + transitionParams.endY[k] = yPoint; + } + } + } + } + + } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/data/ChartData.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/data/ChartData.java index 7ea6f0737..2f2d72d7d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/data/ChartData.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/data/ChartData.java @@ -1,6 +1,7 @@ package org.telegram.ui.Charts.data; import android.graphics.Color; +import android.text.TextUtils; import androidx.core.graphics.ColorUtils; @@ -77,7 +78,7 @@ public class ChartData { Matcher matcher = colorPattern.matcher(colors.getString(line.id)); if (matcher.matches()) { String key = matcher.group(1); - if (key != null) { + if (!TextUtils.isEmpty(key)) { line.colorKey = "statisticChartLine_" + matcher.group(1).toLowerCase(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/data/StackLinearChartData.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/data/StackLinearChartData.java index b2d892a12..dd62886d3 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/data/StackLinearChartData.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/data/StackLinearChartData.java @@ -16,9 +16,36 @@ public class StackLinearChartData extends ChartData { public int simplifiedSize; - public StackLinearChartData(JSONObject jsonObject) throws JSONException { + public StackLinearChartData(JSONObject jsonObject,boolean isLanguages) throws JSONException { super(jsonObject); + if (isLanguages) { + long[] totalCount = new long[lines.size()]; + int[] emptyCount = new int[lines.size()]; + long total = 0; + for (int k = 0; k < lines.size(); k++) { + int n = x.length; + for (int i = 0; i < n; i++) { + int v = lines.get(k).y[i]; + totalCount[k] += v; + if (v == 0) { + emptyCount[k]++; + } + } + total += totalCount[k]; + } + + ArrayList removed = new ArrayList<>(); + for (int k = 0; k < lines.size(); k++) { + if ((totalCount[k] / (double) total) < 0.01 && emptyCount[k] > (x.length / 2f)) { + removed.add(lines.get(k)); + } + } + for (Line r : removed) { + lines.remove(r); + } + } + int n = lines.get(0).y.length; int k = lines.size(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/ChartHeaderView.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/ChartHeaderView.java index 074cf7ca8..7df9b9b2a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/ChartHeaderView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/ChartHeaderView.java @@ -30,6 +30,7 @@ public class ChartHeaderView extends FrameLayout { private TextView datesTmp; public TextView back; private boolean showDate = true; + private boolean useWeekInterval; private Drawable zoomIcon; @@ -99,6 +100,9 @@ public class ChartHeaderView extends FrameLayout { datesTmp.setVisibility(GONE); return; } + if (useWeekInterval) { + end += 86400000L * 7; + } final String newText; if (end - start >= 86400000L) { newText = formatter.format(new Date(start)) + " — " + formatter.format(new Date(end)); @@ -185,6 +189,10 @@ public class ChartHeaderView extends FrameLayout { } } + public void setUseWeekInterval(boolean useWeekInterval) { + this.useWeekInterval = useWeekInterval; + } + public void showDate(boolean b) { showDate = b; if (!showDate) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/LegendSignatureView.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/LegendSignatureView.java index cd52df704..4a6af0b36 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/LegendSignatureView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/LegendSignatureView.java @@ -3,7 +3,6 @@ package org.telegram.ui.Charts.view_data; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; -import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Build; import android.transition.ChangeBounds; @@ -20,10 +19,10 @@ import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.R; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Charts.data.ChartData; import org.telegram.ui.Components.CombinedDrawable; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RadialProgressView; -import org.telegram.ui.Charts.data.ChartData; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -43,8 +42,11 @@ public class LegendSignatureView extends FrameLayout { SimpleDateFormat format = new SimpleDateFormat("E, "); SimpleDateFormat format2 = new SimpleDateFormat("MMM dd"); + SimpleDateFormat format3 = new SimpleDateFormat("d MMM yyyy"); + SimpleDateFormat format4 = new SimpleDateFormat("d MMM"); SimpleDateFormat hourFormat = new SimpleDateFormat(" HH:mm"); + public boolean useWeek; public boolean useHour; public boolean showPercentage; public boolean zoomEnabled; @@ -127,10 +129,10 @@ public class LegendSignatureView extends FrameLayout { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { TransitionSet transition = new TransitionSet(); transition. - addTransition(new Fade(Fade.OUT).setDuration(100)). + addTransition(new Fade(Fade.OUT).setDuration(150)). addTransition(new ChangeBounds().setDuration(150)). - addTransition(new Fade(Fade.IN).setDuration(100)); - transition.setOrdering(TransitionSet.ORDERING_SEQUENTIAL); + addTransition(new Fade(Fade.IN).setDuration(150)); + transition.setOrdering(TransitionSet.ORDERING_TOGETHER); TransitionManager.beginDelayedTransition(this, transition); } } @@ -138,7 +140,11 @@ public class LegendSignatureView extends FrameLayout { if (isTopHourChart) { time.setText(String.format(Locale.ENGLISH, "%02d:00", date)); } else { - time.setText(formatData(new Date(date))); + if (useWeek) { + time.setText(String.format("%s — %s", format4.format(new Date(date)), format3.format(new Date(date + 86400000L * 7)))); + } else { + time.setText(formatData(new Date(date))); + } if (useHour) hourTime.setText(hourFormat.format(date)); } @@ -237,6 +243,10 @@ public class LegendSignatureView extends FrameLayout { } } + public void setUseWeek(boolean useWeek) { + this.useWeek = useWeek; + } + class Holder { final TextView value; final TextView signature; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/TransitionParams.java b/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/TransitionParams.java index c566ea896..8d7f10ae4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/TransitionParams.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Charts/view_data/TransitionParams.java @@ -14,4 +14,10 @@ public class TransitionParams { public float progress; + public float startX[]; + public float startY[]; + public float endX[]; + public float endY[]; + + public float angle[]; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 88959098d..922dd8365 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -175,6 +175,7 @@ import org.telegram.ui.Components.ChatAttachAlert; import org.telegram.ui.Components.ChatAttachAlertDocumentLayout; import org.telegram.ui.Components.ChatAvatarContainer; import org.telegram.ui.Components.ChatBigEmptyView; +import org.telegram.ui.Components.ChatGreetingsView; import org.telegram.ui.Components.ClippingImageView; import org.telegram.ui.Components.CombinedDrawable; import org.telegram.ui.Components.CorrectlyMeasuringTextView; @@ -256,7 +257,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private View progressView2; private FrameLayout bottomOverlay; protected ChatActivityEnterView chatActivityEnterView; - int chatActivityEnterViewAnimateFromTop; + private int chatActivityEnterViewAnimateFromTop; + private boolean chatActivityEnterViewAnimateBeforeSending; private View timeItem2; private ActionBarMenuItem attachItem; private ActionBarMenuItem headerItem; @@ -280,6 +282,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private TextView forwardButton; private TextView replyButton; private FrameLayout emptyViewContainer; + private ChatGreetingsView greetingsViewContainer; private SizeNotifierFrameLayout contentView; private ChatBigEmptyView bigEmptyView; private ArrayList actionModeViews = new ArrayList<>(); @@ -315,6 +318,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private FrameLayout topChatPanelView; private AnimatorSet reportSpamViewAnimator; private TextView addToContactsButton; + private boolean addToContactsButtonArchive; private TextView reportSpamButton; private ImageView closeReportSpam; private FragmentContextView fragmentContextView; @@ -357,6 +361,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private ImageView searchDownButton; private SimpleTextView searchCountText; private ChatActionCell floatingDateView; + private ChatActionCell distanseTopView; private int hideDateDelay = 500; private InstantCameraView instantCameraView; private View overlayView; @@ -383,6 +388,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private int scheduledMessagesCount = -1; private ArrayList animatingMessageObjects = new ArrayList<>(); + private HashMap animatingDocuments = new HashMap<>(); private MessageObject needAnimateToMessage; private int scrollToPositionOnRecreate = -1; @@ -459,6 +465,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private Runnable pendingWebPageTimeoutRunnable; private Runnable waitingForCharaterEnterRunnable; + private TLRPC.ChatInvite chatInvite; + private Runnable chatInviteRunnable; + private boolean clearingHistory; private boolean openAnimationEnded; @@ -584,6 +593,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public static Pattern privateMsgUrlPattern; private boolean waitingForSendingMessageLoad; private ValueAnimator changeBoundAnimator; + private Animator messageEditTextAnimator; + + private int distanceToPeer; + + private int chatListViewPaddingTop; + private int contentPaddingTop; + private int contentPanTranslation; + private float floatingDateViewOffset; + private float topChatPanelViewOffset; + private float pinnedMessageEnterOffset; + private float distanceTopViewOffset; + protected TLRPC.Document preloadedGreetingsSticker; private int transitionAnimationIndex; private final static int[] allowedNotificationsDuringChatListAnimations = new int[]{ @@ -603,6 +624,19 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.didApplyNewTheme }; + private NotificationCenter.PostponeNotificationCallback postponeNotificationsWhileLoadingCallback = new NotificationCenter.PostponeNotificationCallback() { + @Override + public boolean needPostpone(int id, int currentAccount, Object[] args) { + if (id == NotificationCenter.didReceiveNewMessages) { + long did = (Long) args[0]; + if (firstLoading && did == dialog_id) { + return true; + } + } + return false; + } + }; + private interface ChatActivityDelegate { void openReplyMessage(int mid); @@ -648,6 +682,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not textLayoutOut = textLayout; layoutTextWidth = (int) Math.ceil(layoutPaint.measureText(text, 0, text.length())); textLayout = new StaticLayout(text, layoutPaint, layoutTextWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); + setContentDescription(text); invalidate(); if (textLayoutOut != null) { @@ -663,12 +698,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not replaceAnimator.setDuration(150); replaceAnimator.start(); } - } public void setText(CharSequence text) { layoutTextWidth = (int) Math.ceil(layoutPaint.measureText(text, 0, text.length())); textLayout = new StaticLayout(text, layoutPaint, layoutTextWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); + setContentDescription(text); invalidate(); } @@ -701,8 +736,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (event.getAction() == MotionEvent.ACTION_DOWN) { if (textLayout != null) { int lineWidth = (int) Math.ceil(textLayout.getLineWidth(0)); - int contentWidth = lineWidth + (circleWidth > 0 ? circleWidth + AndroidUtilities.dp(8) : 0); - contentWidth += AndroidUtilities.dp(48); + int contentWidth; + if (getMeasuredWidth() == ((View)getParent()).getMeasuredWidth()) { + contentWidth = getMeasuredWidth() - AndroidUtilities.dp(96); + } else { + if (botInfo != null) { + contentWidth = getMeasuredWidth(); + } else { + contentWidth = lineWidth + (circleWidth > 0 ? circleWidth + AndroidUtilities.dp(8) : 0); + contentWidth += AndroidUtilities.dp(48); + } + } int x = (getMeasuredWidth() - contentWidth) / 2; rect.set( x, getMeasuredHeight() / 2f - contentWidth / 2f, @@ -845,7 +889,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } 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) { + if (photoSize.location != null && photoSize.location.volume_id == fileLocation.volume_id && photoSize.location.local_id == fileLocation.local_id) { imageReceiver = cell.getPhotoImage(); break; } @@ -861,7 +905,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not object.viewX = coords[0]; object.viewY = coords[1] - (Build.VERSION.SDK_INT >= 21 ? 0 : AndroidUtilities.statusBarHeight); object.parentView = chatListView; - object.animatingImageView = !SharedConfig.smoothKeyboard && pagedownButton != null && pagedownButton.getTag() != null ? animatingImageView : null; + object.animatingImageView = !SharedConfig.smoothKeyboard && pagedownButton != null && pagedownButton.getTag() != null && view instanceof ChatMessageCell ? animatingImageView : null; object.imageReceiver = imageReceiver; if (needPreview) { object.thumb = imageReceiver.getBitmapSafe(); @@ -1157,6 +1201,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not MediaController.getInstance().startMediaObserver(); } + getNotificationCenter().addPostponeNotificationsCallback(postponeNotificationsWhileLoadingCallback); + if (!inScheduleMode) { getNotificationCenter().addObserver(this, NotificationCenter.messagesRead); getNotificationCenter().addObserver(this, NotificationCenter.screenshotTook); @@ -1302,6 +1348,30 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + if (chatInvite != null) { + int timeout = chatInvite.expires - getConnectionsManager().getCurrentTime(); + if (timeout < 0) { + timeout = 10; + } + AndroidUtilities.runOnUIThread(chatInviteRunnable = () -> { + chatInviteRunnable = null; + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + if (ChatObject.isChannel(currentChat) && !currentChat.megagroup) { + builder.setMessage(LocaleController.getString("JoinByPeekChannelText", R.string.JoinByPeekChannelText)); + builder.setTitle(LocaleController.getString("JoinByPeekChannelTitle", R.string.JoinByPeekChannelTitle)); + } else { + builder.setMessage(LocaleController.getString("JoinByPeekGroupText", R.string.JoinByPeekGroupText)); + builder.setTitle(LocaleController.getString("JoinByPeekGroupTitle", R.string.JoinByPeekGroupTitle)); + } + builder.setPositiveButton(LocaleController.getString("JoinByPeekJoin", R.string.JoinByPeekJoin), (dialogInterface, i) -> { + if (bottomOverlayChatText != null) { + bottomOverlayChatText.callOnClick(); + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), (dialogInterface, i) -> finishFragment()); + showDialog(builder.create()); + }, timeout * 1000); + } return true; } @@ -1326,6 +1396,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (topUndoView != null) { topUndoView.hide(true, 0); } + if (chatInviteRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(chatInviteRunnable); + chatInviteRunnable = null; + } + getNotificationCenter().removePostponeNotificationsCallback(postponeNotificationsWhileLoadingCallback); getMessagesController().setLastCreatedDialogId(dialog_id, inScheduleMode, false); getNotificationCenter().removeObserver(this, NotificationCenter.messagesDidLoad); NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.emojiDidLoad); @@ -1614,32 +1689,19 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { clearingHistory = true; undoView.showWithAction(dialog_id, id == clear_history ? UndoView.ACTION_CLEAR : UndoView.ACTION_DELETE, () -> { - if (id == clear_history) { - if (chatInfo != null && chatInfo.pinned_msg_id != 0) { - SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); + if (chatInfo != null && chatInfo.pinned_msg_id != 0) { + SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); preferences.edit().putInt("pin_" + dialog_id, chatInfo.pinned_msg_id).apply(); - updatePinnedMessageView(true); - } else if (userInfo != null && userInfo.pinned_msg_id != 0) { - SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); + updatePinnedMessageView(true); + } else if (userInfo != null && userInfo.pinned_msg_id != 0) { + SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); preferences.edit().putInt("pin_" + dialog_id, userInfo.pinned_msg_id).apply(); - updatePinnedMessageView(true); - } - getMessagesController().deleteDialog(dialog_id, 1, param); - clearingHistory = false; - clearHistory(false); - chatAdapter.notifyDataSetChanged(); - } else { - if (isChat) { - if (ChatObject.isNotInChat(currentChat)) { - getMessagesController().deleteDialog(dialog_id, 0, param); - } else { - getMessagesController().deleteUserFromChat((int) -dialog_id, getMessagesController().getUser(getUserConfig().getClientUserId()), null); - } - } else { - getMessagesController().deleteDialog(dialog_id, 0, param); - } - finishFragment(); + updatePinnedMessageView(true); } + getMessagesController().deleteDialog(dialog_id, 1, param); + clearingHistory = false; + clearHistory(false); + chatAdapter.notifyDataSetChanged(); }, () -> { clearingHistory = false; chatAdapter.notifyDataSetChanged(); @@ -1782,7 +1844,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not avatarContainer.onDestroy(); } avatarContainer = new ChatAvatarContainer(context, this, currentEncryptedChat != null); - if (inPreviewMode) { + if (inPreviewMode || inBubbleMode) { avatarContainer.setOccupyStatusBar(false); } actionBar.addView(avatarContainer, 0, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, !inPreviewMode ? 56 : 0, 0, 40, 0)); @@ -2298,13 +2360,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int clipLeft = 0; int clipBottom = 0; - if (position != null) { - if (position.pw != position.spanSize && position.spanSize == 1000 && position.siblingHeights == null && group.hasSibling) { - clipLeft = cell.getBackgroundDrawableLeft(); - } else if (position.siblingHeights != null) { - clipBottom = child.getBottom() - AndroidUtilities.dp(1 + (cell.isPinnedBottom() ? 1 : 0)); - } - } + float viewClipLeft; float viewClipRight; float viewClipTop; @@ -2426,7 +2482,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int keyboardSize = SharedConfig.smoothKeyboard ? 0 : getKeyboardHeight(); if (keyboardSize <= AndroidUtilities.dp(20)) { - if (!AndroidUtilities.isInMultiwindow && !SharedConfig.smoothKeyboard) { + if (!inBubbleMode && !AndroidUtilities.isInMultiwindow && !SharedConfig.smoothKeyboard) { heightSize -= chatActivityEnterView.getEmojiPadding(); allHeight -= chatActivityEnterView.getEmojiPadding(); } @@ -2490,7 +2546,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int contentHeightSpec = MeasureSpec.makeMeasureSpec(allHeight - actionBarHeight - AndroidUtilities.dp(48), MeasureSpec.EXACTLY); child.measure(contentWidthSpec, contentHeightSpec); } else if (chatActivityEnterView.isPopupView(child)) { - if (AndroidUtilities.isInMultiwindow) { + if (inBubbleMode) { + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize - inputFieldHeight + actionBarHeight + getPaddingTop(), MeasureSpec.EXACTLY)); + } else if (AndroidUtilities.isInMultiwindow) { if (AndroidUtilities.isTablet()) { child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(Math.min(AndroidUtilities.dp(320), heightSize - inputFieldHeight + actionBarHeight - AndroidUtilities.statusBarHeight + getPaddingTop()), MeasureSpec.EXACTLY)); } else { @@ -2579,7 +2637,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); int keyboardSize = SharedConfig.smoothKeyboard ? 0 : getKeyboardHeight(); - int paddingBottom = keyboardSize <= AndroidUtilities.dp(20) && !AndroidUtilities.isInMultiwindow ? chatActivityEnterView.getEmojiPadding() : 0; + int paddingBottom = keyboardSize <= AndroidUtilities.dp(20) && !AndroidUtilities.isInMultiwindow && !inBubbleMode ? chatActivityEnterView.getEmojiPadding() : 0; if (!SharedConfig.smoothKeyboard) { setBottomClip(paddingBottom); } @@ -2652,7 +2710,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else if (child == emptyViewContainer) { childTop -= inputFieldHeight / 2 - (actionBar.getVisibility() == VISIBLE ? actionBar.getMeasuredHeight() / 2 : 0); } else if (chatActivityEnterView.isPopupView(child)) { - if (AndroidUtilities.isInMultiwindow) { + if (AndroidUtilities.isInMultiwindow || inBubbleMode) { childTop = chatActivityEnterView.getTop() - child.getMeasuredHeight() + AndroidUtilities.dp(1); } else { childTop = chatActivityEnterView.getBottom(); @@ -2694,19 +2752,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private void setNonNoveTranslation(int y) { contentView.setTranslationY(y); actionBar.setTranslationY(0); - + contentPanTranslation = 0; contentView.setBackgroundTranslation(0); - if (pinnedMessageView != null) { - pinnedMessageView.setTranslationY(0); - } - if (fragmentContextView != null) { - fragmentContextView.setTranslationY(0); - } - if (fragmentLocationContextView != null) { - fragmentLocationContextView.setTranslationY(0); - } - topChatPanelView.setTranslationY(0); instantCameraView.onPanTranslationUpdate(0); + updateChatListViewTopPadding(); } @Override @@ -2714,27 +2763,29 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (getParentLayout() != null && getParentLayout().isPreviewOpenAnimationInProgress()) { return; } + contentPanTranslation = y; if (chatAttachAlert != null && chatAttachAlert.isShowing() || chatActivityEnterView.isPopupShowing()) { setNonNoveTranslation(y); } else { actionBar.setTranslationY(y); contentView.setBackgroundTranslation(y); - if (pinnedMessageView != null) { - pinnedMessageView.setTranslationY(y); - } - if (fragmentContextView != null) { - fragmentContextView.setTranslationY(y); - } - if (fragmentLocationContextView != null) { - fragmentLocationContextView.setTranslationY(y); - } - topChatPanelView.setTranslationY(y); instantCameraView.onPanTranslationUpdate(y); + updateChatListViewTopPadding(); } } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + contentPaddingTop = top; + updateChatListViewTopPadding(); + } + }; contentView = (SizeNotifierFrameLayout) fragmentView; + if (inBubbleMode) { + contentView.setOccupyStatusBar(false); + } contentView.setBackgroundImage(Theme.getCachedWallpaper(), Theme.isWallpaperMotion()); @@ -2743,7 +2794,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not contentView.addView(emptyViewContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); emptyViewContainer.setOnTouchListener((v, event) -> true); - if (currentEncryptedChat == null) { + int distance = getArguments().getInt("nearby_distance", -1); + if (distance >= 0 && currentUser != null) { + greetingsViewContainer = new ChatGreetingsView(context, currentUser, distance, preloadedGreetingsSticker); + greetingsViewContainer.setListener((sticker) -> { + animatingDocuments.put(sticker, 0); + SendMessagesHelper.getInstance(currentAccount).sendSticker(sticker, dialog_id, null, null, true, 0); + }); + emptyViewContainer.addView(greetingsViewContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_VERTICAL, 68, 0, 68, 0)); + } else if (currentEncryptedChat == null) { if (!inScheduleMode && (currentUser != null && currentUser.self || currentChat != null && currentChat.creator)) { bigEmptyView = new ChatBigEmptyView(context, currentChat != null ? ChatBigEmptyView.EMPTY_VIEW_TYPE_GROUP : ChatBigEmptyView.EMPTY_VIEW_TYPE_SAVED); emptyViewContainer.addView(bigEmptyView, new FrameLayout.LayoutParams(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); @@ -2824,6 +2883,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not super.requestLayout(); } + @Override + public void setTranslationY(float translationY) { + super.setTranslationY(translationY); + if (emptyViewContainer != null) { + emptyViewContainer.setTranslationY(translationY / 2f); + } + } + @Override protected void onMeasure(int widthSpec, int heightSpec) { int height = MeasureSpec.getSize(heightSpec); @@ -2883,7 +2950,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public boolean onInterceptTouchEvent(MotionEvent e) { textSelectionHelper.checkSelectionCancel(e); - if (isScrollAnimationRunning()) { + if (isFastScrollAnimationRunning()) { return false; } boolean result = super.onInterceptTouchEvent(e); @@ -2894,6 +2961,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return result; } + @Override + public void setItemAnimator(ItemAnimator animator) { + if (isFastScrollAnimationRunning()) { + return; + } + super.setItemAnimator(animator); + } + private void drawReplyButton(Canvas canvas) { if (slidingView == null) { return; @@ -3033,7 +3108,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public boolean onTouchEvent(MotionEvent e) { textSelectionHelper.checkSelectionCancel(e); - if (isScrollAnimationRunning()) { + if (isFastScrollAnimationRunning()) { return false; } boolean result = super.onTouchEvent(e); @@ -3105,7 +3180,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not drawReplyButton(c); } - if (chatListView.isScrollAnimationRunning()) { + if (chatListView.isFastScrollAnimationRunning()) { updateTextureViewPosition(false); } } @@ -3124,7 +3199,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (chatAdapter.isBot && child instanceof BotHelpCell) { BotHelpCell botCell = (BotHelpCell) child; int top = getMeasuredHeight() / 2 - child.getMeasuredHeight() / 2; - if (!botCell.animating()) { + if (!botCell.animating() && !chatListView.fastScrollAnimationRunning) { if (child.getTop() > top) { child.setTranslationY(top - child.getTop()); } else { @@ -3202,14 +3277,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } for (int k = 0; k < 3; k++) { drawingGroups.clear(); - if (k == 2 && !chatListView.isScrollAnimationRunning()) { + if (k == 2 && !chatListView.isFastScrollAnimationRunning()) { continue; } for (int i = 0; i < count; i++) { View child = chatListView.getChildAt(i); if (child instanceof ChatMessageCell) { ChatMessageCell cell = (ChatMessageCell) child; - if (chatListView.isScrollAnimationRunning()) { + if (chatListView.isFastScrollAnimationRunning()) { if (child.getY() > chatListView.getHeight() || child.getY() + child.getHeight() < 0) { continue; } @@ -3364,6 +3439,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (clipToGroupBounds) { canvas.restore(); } + if (cell != null && cell.getTransitionParams().animateBackgroundBoundsInner) { + canvas.save(); + canvas.translate(cell.getX(), cell.getY()); + cell.drawOutboundsContent(canvas); + canvas.restore(); + } } else { result = false; } @@ -3543,10 +3624,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (child.getTranslationY() != 0) { canvas.restore(); } + imageReceiver.setVisible(false, false); return result; } - boolean replaceAnimation = chatListView.isScrollAnimationRunning() || (groupedMessages != null && groupedMessages.transitionParams.backgroundChangeBounds); + boolean replaceAnimation = chatListView.isFastScrollAnimationRunning() || (groupedMessages != null && groupedMessages.transitionParams.backgroundChangeBounds); int top = replaceAnimation ? child.getTop() : (int) child.getY(); if (chatMessageCell.isPinnedBottom()) { int p; @@ -3586,6 +3668,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (child.getTranslationY() != 0) { canvas.restore(); } + imageReceiver.setVisible(false, false); return result; } } else { @@ -3594,6 +3677,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (child.getTranslationY() != 0) { canvas.restore(); } + imageReceiver.setVisible(false, false); return result; } } @@ -3690,9 +3774,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } } - if (top >= 0 && y - AndroidUtilities.dp(48) < top) { + if (y - AndroidUtilities.dp(48) < top) { y = top + AndroidUtilities.dp(48); } + if (!chatMessageCell.isPinnedBottom()) { + int cellBottom = replaceAnimation ? chatMessageCell.getBottom() : (int) (chatMessageCell.getY() + chatMessageCell.getMeasuredHeight()); + if (y > cellBottom) { + y = cellBottom; + } + } if (tx != 0) { canvas.save(); canvas.translate(tx, 0); @@ -3708,6 +3798,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { imageReceiver.setAlpha(1f); } + imageReceiver.setVisible(true, false); imageReceiver.draw(canvas); if (tx != 0) { canvas.restore(); @@ -3757,7 +3848,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatListView.setVerticalScrollBarEnabled(true); chatListView.setAdapter(chatAdapter = new ChatActivityAdapter(context)); chatListView.setClipToPadding(false); - chatListView.setPadding(0, AndroidUtilities.dp(4), 0, AndroidUtilities.dp(3)); + chatListView.setAnimateEmptyView(true); + chatListViewPaddingTop = 0; + updateChatListViewTopPadding(); if (MessagesController.getGlobalMainSettings().getBoolean("view_animations", true)) { chatListItemAniamtor = new ChatListItemAnimator(this, chatListView) { @@ -3947,7 +4040,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not checkTextureViewPosition = false; hideFloatingDateView(true); checkAutoDownloadMessages(scrollUp); - if (SharedConfig.getDevicePerfomanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW) { + if (SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW) { NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.startAllHeavyOperations, 512); } chatListView.setOverScrollMode(RecyclerView.OVER_SCROLL_ALWAYS); @@ -3967,7 +4060,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not checkTextureViewPosition = true; scrollingChatListView = true; } - if (SharedConfig.getDevicePerfomanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW) { + if (SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW) { NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.stopAllHeavyOperations, 512); } } @@ -4064,6 +4157,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }; floatingDateView.setCustomDate((int) (System.currentTimeMillis() / 1000), false, false); floatingDateView.setAlpha(0.0f); + floatingDateView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); contentView.addView(floatingDateView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 4, 0, 0)); floatingDateView.setOnClickListener(view -> { if (floatingDateView.getAlpha() == 0 || actionBar.isActionModeShowed()) { @@ -4125,7 +4219,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (currentEncryptedChat == null) { pinnedMessageView = new FrameLayout(context); pinnedMessageView.setTag(1); - pinnedMessageView.setTranslationY(-AndroidUtilities.dp(50)); + pinnedMessageEnterOffset = -AndroidUtilities.dp(50); pinnedMessageView.setVisibility(View.GONE); pinnedMessageView.setBackgroundResource(R.drawable.blockpanel); pinnedMessageView.getBackground().setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_topPanelBackground), PorterDuff.Mode.SRC_IN)); @@ -4139,6 +4233,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); + + View selector = new View(context); + selector.setBackground(Theme.getSelectorDrawable(false)); + pinnedMessageView.addView(selector, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 2)); + pinnedLineView = new View(context); pinnedLineView.setBackgroundColor(Theme.getColor(Theme.key_chat_topPanelLine)); pinnedMessageView.addView(pinnedLineView, LayoutHelper.createFrame(2, 32, Gravity.LEFT | Gravity.TOP, 8, 8, 0, 0)); @@ -4162,6 +4261,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not closePinned.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_topPanelClose), PorterDuff.Mode.SRC_IN)); closePinned.setScaleType(ImageView.ScaleType.CENTER); closePinned.setContentDescription(LocaleController.getString("Close", R.string.Close)); + if (Build.VERSION.SDK_INT >= 21) { + closePinned.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_inappPlayerClose) & 0x19ffffff, 1, AndroidUtilities.dp(14))); + } pinnedMessageView.addView(closePinned, LayoutHelper.createFrame(36, 48, Gravity.RIGHT | Gravity.TOP)); closePinned.setOnClickListener(v -> { if (getParentActivity() == null) { @@ -4222,8 +4324,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) reportSpamButton.getLayoutParams(); layoutParams.width = width; if (addToContactsButton != null && addToContactsButton.getVisibility() == VISIBLE) { - reportSpamButton.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(19), 0); + reportSpamButton.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4), 0); layoutParams.leftMargin = width; + layoutParams.width -= AndroidUtilities.dp(15); } else { reportSpamButton.setPadding(AndroidUtilities.dp(48), 0, AndroidUtilities.dp(48), 0); layoutParams.leftMargin = 0; @@ -4252,7 +4355,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }; topChatPanelView.setTag(1); - topChatPanelView.setTranslationY(-AndroidUtilities.dp(50)); + topChatPanelViewOffset = -AndroidUtilities.dp(50); + updateChatListViewTopPadding(); topChatPanelView.setVisibility(View.GONE); topChatPanelView.setBackgroundResource(R.drawable.blockpanel); topChatPanelView.getBackground().setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_topPanelBackground), PorterDuff.Mode.SRC_IN)); @@ -4260,6 +4364,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not reportSpamButton = new TextView(context); reportSpamButton.setTextColor(Theme.getColor(Theme.key_chat_reportSpam)); + if (Build.VERSION.SDK_INT >= 21) { + reportSpamButton.setBackground(Theme.createSelectorDrawable(Theme.getColor(Theme.key_chat_reportSpam) & 0x19ffffff, 5)); + } reportSpamButton.setTag(Theme.key_chat_reportSpam); reportSpamButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); reportSpamButton.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); @@ -4284,9 +4391,23 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not addToContactsButton.setMaxLines(1); addToContactsButton.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4), 0); addToContactsButton.setGravity(Gravity.CENTER); + if (Build.VERSION.SDK_INT >= 21) { + addToContactsButton.setBackground(Theme.createSelectorDrawable(Theme.getColor(Theme.key_chat_addContact) & 0x19ffffff, 5)); + } topChatPanelView.addView(addToContactsButton, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, AndroidUtilities.dp(1))); addToContactsButton.setOnClickListener(v -> { - if (addToContactsButton.getTag() != null) { + if (addToContactsButtonArchive) { + getMessagesController().addDialogToFolder(dialog_id, 0, 0, 0); + undoView.showWithAction(dialog_id, UndoView.ACTION_CHAT_UNARCHIVED, null); + SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean("dialog_bar_archived" + dialog_id, false); + editor.putBoolean("dialog_bar_block" + dialog_id, false); + editor.putBoolean("dialog_bar_report" + dialog_id, false); + editor.commit(); + updateTopPanel(false); + getNotificationsController().clearDialogNotificationsSettings(dialog_id); + } else if (addToContactsButton.getTag() != null) { shareMyContact(1, null); } else { Bundle args = new Bundle(); @@ -4300,11 +4421,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not closeReportSpam = new ImageView(context); closeReportSpam.setImageResource(R.drawable.miniplayer_close); + if (Build.VERSION.SDK_INT >= 21) { + closeReportSpam.setBackground(Theme.createSelectorDrawable(Theme.getColor(Theme.key_chat_topPanelClose) & 0x19ffffff)); + } closeReportSpam.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_topPanelClose), PorterDuff.Mode.SRC_IN)); closeReportSpam.setScaleType(ImageView.ScaleType.CENTER); topChatPanelView.addView(closeReportSpam, LayoutHelper.createFrame(48, 48, Gravity.RIGHT | Gravity.TOP)); closeReportSpam.setOnClickListener(v -> { - getMessagesController().hidePeerSettingsBar(dialog_id, currentUser, currentChat); + long did = dialog_id; + if (currentEncryptedChat != null) { + did = currentUser.id; + } + getMessagesController().hidePeerSettingsBar(did, currentUser, currentChat); updateTopPanel(true); }); @@ -4344,7 +4472,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else if (returnToMessageId > 0) { scrollToMessageId(returnToMessageId, 0, true, returnToLoadIndex, true); } else { - scrollToLastMessage(true); + scrollToLastMessage(); } }); @@ -4778,7 +4906,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (result.type.equals("video") || result.type.equals("web_player_video")) { int[] size = MessageObject.getInlineResultWidthAndHeight(result); - EmbedBottomSheet.show(getParentActivity(), result.title != null ? result.title : "", result.description, result.content.url, result.content.url, size[0], size[1]); + EmbedBottomSheet.show(getParentActivity(), result.title != null ? result.title : "", result.description, result.content.url, result.content.url, size[0], size[1], isKeyboardVisible()); } else { if (AndroidUtilities.shouldShowUrlInAlert(result.content.url)) { AlertsCreator.showOpenUrlAlert(ChatActivity.this, result.content.url, true, true, true); @@ -4805,23 +4933,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int len = mentionsAdapter.getResultLength(); if (object instanceof TLRPC.User) { if (searchingForUser && searchContainer.getVisibility() == View.VISIBLE) { - searchingUserMessages = (TLRPC.User) object; - if (searchingUserMessages == null) { - return; - } - String name = searchingUserMessages.first_name; - if (TextUtils.isEmpty(name)) { - name = searchingUserMessages.last_name; - } - searchingForUser = false; - String from = LocaleController.getString("SearchFrom", R.string.SearchFrom); - Spannable spannable = new SpannableString(from + " " + name); - spannable.setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_actionBarDefaultSubtitle)), from.length() + 1, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - searchItem.setSearchFieldCaption(spannable); - mentionsAdapter.searchUsernameOrHashtag(null, 0, null, false); - searchItem.setSearchFieldHint(null); - searchItem.clearSearchText(); - getMediaDataController().searchMessagesInChat("", dialog_id, mergeDialogId, classGuid, 0, searchingUserMessages); + searchUserMessages((TLRPC.User) object); } else { TLRPC.User user = (TLRPC.User) object; if (user != null) { @@ -5089,6 +5201,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatActivityEnterView = new ChatActivityEnterView(getParentActivity(), contentView, this, true) { int lastContentViewHeight; + int messageEditTextPredrawHeigth; + int messageEditTextPredrawScrollY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { @@ -5115,15 +5229,27 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - int t = getBackgroundTop(); - if (chatActivityEnterViewAnimateFromTop != 0 && t != chatActivityEnterViewAnimateFromTop && lastContentViewHeight == contentView.getMeasuredHeight()) { + public void draw(Canvas canvas) { + if (actionBar.isActionModeShowed()) { + if (messageEditTextAnimator != null) { + messageEditTextAnimator.cancel(); + } if (changeBoundAnimator != null) { changeBoundAnimator.cancel(); } - int dy = chatActivityEnterViewAnimateFromTop - t; + chatActivityEnterViewAnimateFromTop = 0; + shouldAnimateEditTextWithBounds = false; + super.draw(canvas); + return; + } + int t = getBackgroundTop(); + if (chatActivityEnterViewAnimateFromTop != 0 && t != chatActivityEnterViewAnimateFromTop && lastContentViewHeight == contentView.getMeasuredHeight()) { + int dy = animatedTop + chatActivityEnterViewAnimateFromTop - t; animatedTop = dy; + if (changeBoundAnimator != null) { + changeBoundAnimator.cancel(); + } + chatListView.setTranslationY(dy); if (topView != null && topView.getVisibility() == View.VISIBLE) { topView.setTranslationY(animatedTop + (1f - topViewEnterProgress) * topView.getLayoutParams().height); @@ -5131,7 +5257,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not topLineView.setTranslationY(animatedTop); } } - contentView.setClipChildren(false); changeBoundAnimator = ValueAnimator.ofFloat(1f, 0); @@ -5144,6 +5269,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not topLineView.setTranslationY(animatedTop); } } else { + if (mentionContainer != null) { + mentionContainer.setTranslationY(v); + } + if (mentiondownButton != null && mentiondownButton.getTag() != null) { + mentiondownButton.setTranslationY(pagedownButton.getVisibility() != VISIBLE ? v : v - AndroidUtilities.dp(72)); + } + if (pagedownButton != null && pagedownButton.getTag() != null) { + pagedownButton.setTranslationY(v); + } chatListView.setTranslationY(v); } invalidate(); @@ -5165,14 +5299,51 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); changeBoundAnimator.setDuration(200); - changeBoundAnimator.setStartDelay(20); + if (chatActivityEnterViewAnimateBeforeSending) { + changeBoundAnimator.setStartDelay(20); + } changeBoundAnimator.setInterpolator(CubicBezierInterpolator.DEFAULT); if (!waitingForSendingMessageLoad) { changeBoundAnimator.start(); } } + if (shouldAnimateEditTextWithBounds) { + float dy = (messageEditTextPredrawHeigth - messageEditText.getMeasuredHeight()) + (messageEditTextPredrawScrollY - messageEditText.getScrollY()); + messageEditText.setOffsetY(messageEditText.getOffsetY() - dy); + ValueAnimator a = ValueAnimator.ofFloat(messageEditText.getOffsetY(), 0); + a.addUpdateListener(animation -> messageEditText.setOffsetY((float) animation.getAnimatedValue())); + if (messageEditTextAnimator != null) { + messageEditTextAnimator.cancel(); + } + messageEditTextAnimator = a; + a.setDuration(200); + a.setStartDelay(chatActivityEnterViewAnimateBeforeSending ? 20 : 0); + a.setInterpolator(CubicBezierInterpolator.DEFAULT); + a.start(); + shouldAnimateEditTextWithBounds = false; + } lastContentViewHeight = contentView.getMeasuredHeight(); chatActivityEnterViewAnimateFromTop = 0; + chatActivityEnterViewAnimateBeforeSending = false; + super.draw(canvas); + } + + @Override + protected void onLineCountChanged(int oldLineCount, int newLineCount) { + if (chatActivityEnterView != null) { + if (!TextUtils.isEmpty(messageEditText.getText())) { + shouldAnimateEditTextWithBounds = true; + int lineHeight = messageEditText.getLineHeight(); + messageEditTextPredrawHeigth = messageEditText.getMeasuredHeight(); + messageEditTextPredrawScrollY = messageEditText.getScrollY(); + invalidate(); + } else { + messageEditText.animate().cancel(); + messageEditText.setOffsetY(0); + shouldAnimateEditTextWithBounds = false; + } + chatActivityEnterViewAnimateFromTop = chatActivityEnterView.getBackgroundTop(); + } } }; chatActivityEnterView.setDelegate(new ChatActivityEnterView.ChatActivityEnterViewDelegate() { @@ -5180,6 +5351,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public void onMessageSend(CharSequence message, boolean notify, int scheduleDate) { if (chatListItemAniamtor != null) { chatActivityEnterViewAnimateFromTop = chatActivityEnterView.getBackgroundTop(); + if (chatActivityEnterViewAnimateFromTop != 0) { + chatActivityEnterViewAnimateBeforeSending = true; + } } if (mentionsAdapter != null) { mentionsAdapter.addHashtagsFromMessage(message); @@ -5328,6 +5502,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public void onMessageEditEnd(boolean loading) { if (chatListItemAniamtor != null) { chatActivityEnterViewAnimateFromTop = chatActivityEnterView.getBackgroundTop(); + if (chatActivityEnterViewAnimateFromTop != 0) { + chatActivityEnterViewAnimateBeforeSending = true; + } } if (!loading) { mentionsAdapter.setNeedBotContext(currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46); @@ -5372,6 +5549,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not allowContextBotPanel = !chatActivityEnterView.isPopupShowing(); checkContextBotPanel(); chatActivityEnterViewAnimateFromTop = 0; + chatActivityEnterViewAnimateBeforeSending = false; } @Override @@ -5586,6 +5764,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not replyCloseImageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_chat_replyPanelClose), PorterDuff.Mode.SRC_IN)); replyCloseImageView.setImageResource(R.drawable.input_clear); replyCloseImageView.setScaleType(ImageView.ScaleType.CENTER); + if (Build.VERSION.SDK_INT >= 21) { + replyCloseImageView.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_inappPlayerClose) & 0x19ffffff, 1, AndroidUtilities.dp(18))); + } replyLayout.addView(replyCloseImageView, LayoutHelper.createFrame(52, 46, Gravity.RIGHT | Gravity.TOP, 0, 0.5f, 0, 0)); replyCloseImageView.setOnClickListener(v -> { if (forwardingMessages != null) { @@ -5642,6 +5823,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not inputStickerSet.access_hash = set.access_hash; inputStickerSet.id = set.id; StickersAlert alert = new StickersAlert(getParentActivity(), ChatActivity.this, inputStickerSet, null, chatActivityEnterView); + alert.setCalcMandatoryInsets(isKeyboardVisible()); alert.setClearsInputField(clearsInputField); showDialog(alert); } @@ -5714,7 +5896,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not searchUpButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_actionBarActionModeDefaultSelector), 1)); searchContainer.addView(searchUpButton, LayoutHelper.createFrame(48, 48, Gravity.RIGHT | Gravity.TOP, 0, 0, 48, 0)); searchUpButton.setOnClickListener(view -> { - if (chatListView.isScrollAnimationRunning()) { + if (chatListView.isFastScrollAnimationRunning()) { return; } getMediaDataController().searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 1, searchingUserMessages); @@ -5734,7 +5916,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not searchDownButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_actionBarActionModeDefaultSelector), 1)); searchContainer.addView(searchDownButton, LayoutHelper.createFrame(48, 48, Gravity.RIGHT | Gravity.TOP, 0, 0, 0, 0)); searchDownButton.setOnClickListener(view -> { - if (chatListView.isScrollAnimationRunning()) { + if (chatListView.isFastScrollAnimationRunning()) { return; } getMediaDataController().searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 2, searchingUserMessages); @@ -5911,6 +6093,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { if (ChatObject.isChannel(currentChat) && !(currentChat instanceof TLRPC.TL_channelForbidden)) { if (ChatObject.isNotInChat(currentChat)) { + if (chatInviteRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(chatInviteRunnable); + chatInviteRunnable = null; + } showBottomOverlayProgress(true, true); getMessagesController().addUserToChat(currentChat.id, getUserConfig().getCurrentUser(), null, 0, null, ChatActivity.this, null); NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.closeSearchByActiveAction); @@ -6038,6 +6224,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not updateSecretStatus(); updateTopPanel(false); updatePinnedMessageView(true); + updateDistanceView(false); chatScrollHelper = new RecyclerAnimationScrollHelper(chatListView, chatLayoutManager); chatScrollHelper.setScrollListener(() -> updateMessagesVisiblePart(false)); @@ -6085,9 +6272,182 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fireworksOverlay = new FireworksOverlay(context); contentView.addView(fireworksOverlay, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); textSelectionHelper.setParentView(chatListView); + + if (getArguments().containsKey("search_from_user_id")) { + TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(getArguments().getInt("search_from_user_id")); + if (user != null) { + openSearchWithText(""); + searchUserButton.callOnClick(); + searchUserMessages(user); + } + } return fragmentView; } + private void searchUserMessages(TLRPC.User user) { + searchingUserMessages = user; + if (searchingUserMessages == null) { + return; + } + String name = searchingUserMessages.first_name; + if (TextUtils.isEmpty(name)) { + name = searchingUserMessages.last_name; + } + searchingForUser = false; + String from = LocaleController.getString("SearchFrom", R.string.SearchFrom); + Spannable spannable = new SpannableString(from + " " + name); + spannable.setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_actionBarDefaultSubtitle)), from.length() + 1, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + searchItem.setSearchFieldCaption(spannable); + mentionsAdapter.searchUsernameOrHashtag(null, 0, null, false); + searchItem.setSearchFieldHint(null); + searchItem.clearSearchText(); + getMediaDataController().searchMessagesInChat("", dialog_id, mergeDialogId, classGuid, 0, searchingUserMessages); + } + + Animator distanceViewAnimator; + + private void updateDistanceView(boolean animated) { + SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); + distanceToPeer = preferences.getInt("dialog_bar_distance" + dialog_id, -1); + + if (distanceToPeer >= 0 && currentUser != null) { + if (distanseTopView == null) { + distanseTopView = new ChatActionCell(contentView.getContext()); + distanseTopView.setCustomText(LocaleController.formatString("ChatDistanceToPeer", R.string.ChatDistanceToPeer, currentUser.first_name, LocaleController.formatDistance(distanceToPeer, 0))); + distanseTopView.setOnClickListener(v -> presentFragment(new PeopleNearbyActivity())); + contentView.addView(distanseTopView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 4, 0, 0)); + } + if (animated) { + if (distanseTopView.getTag() == null) { + ValueAnimator a = ValueAnimator.ofFloat(0, 1f); + distanseTopView.setTag(1); + distanseTopView.setAlpha(0f); + View distanceTopViewFinal = distanseTopView; + a.addUpdateListener(animation -> { + float alpha = (float) animation.getAnimatedValue(); + distanceTopViewOffset = (alpha) * AndroidUtilities.dp(30); + updateChatListViewTopPadding(); + distanceTopViewFinal.setAlpha(alpha); + }); + a.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + distanceTopViewOffset = AndroidUtilities.dp(30); + updateChatListViewTopPadding(); + } + }); + a.setDuration(150); + distanceViewAnimator = a; + a.start(); + } + } else { + distanseTopView.setTag(1); + distanceTopViewOffset = AndroidUtilities.dp(30); + updateChatListViewTopPadding(); + } + } + } + + private void hideDistanceView() { + if (distanceToPeer >= 0) { + distanceToPeer = -1; + SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); + preferences.edit().putInt("dialog_bar_distance" + dialog_id, -2).commit(); + + if (distanceViewAnimator != null) { + distanceViewAnimator.cancel(); + } + + if (distanseTopView != null) { + View distanseTopViewFinal = distanseTopView; + distanseTopView = null; + + ValueAnimator a = ValueAnimator.ofFloat(1f, 0); + a.addUpdateListener(animation -> { + float alpha = (float) animation.getAnimatedValue(); + distanceTopViewOffset = (alpha) * AndroidUtilities.dp(30); + updateChatListViewTopPadding(); + distanseTopViewFinal.setAlpha(alpha); + }); + a.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + distanceTopViewOffset = 0; + if (animation == distanceViewAnimator) { + ViewGroup parent = (ViewGroup) distanseTopViewFinal.getParent(); + if (parent != null) { + parent.removeView(distanseTopViewFinal); + } + distanceViewAnimator = null; + } + } + }); + a.setDuration(150); + distanceViewAnimator = a; + a.start(); + } + } + } + + private void updateChatListViewTopPadding() { + float topPanelViewH = Math.max(0, AndroidUtilities.dp(48) + topChatPanelViewOffset); + float pinnedViewH = 0; + if (pinnedMessageView != null && pinnedMessageView.getVisibility() == View.VISIBLE) { + pinnedViewH = Math.max(0, AndroidUtilities.dp(48) + pinnedMessageEnterOffset); + } + int p = (int) (AndroidUtilities.dp(4) + contentPaddingTop + distanceTopViewOffset + topPanelViewH + pinnedViewH); + int viewsOffset = (int) (contentPaddingTop + topPanelViewH + pinnedViewH); + if (chatListView != null && chatLayoutManager != null && chatAdapter != null) { + if (chatListView.getPaddingTop() != p) { + int firstVisPos = chatLayoutManager.findFirstVisibleItemPosition(); + int lastVisPos = chatLayoutManager.findLastVisibleItemPosition(); + int top = 0; + MessageObject scrollToMessageObject = null; + if (firstVisPos != RecyclerView.NO_POSITION) { + for (int i = firstVisPos; i <= lastVisPos; i++) { + View v = chatLayoutManager.findViewByPosition(i); + if (v instanceof ChatMessageCell) { + scrollToMessageObject = ((ChatMessageCell) v).getMessageObject(); + top = chatListView.getMeasuredHeight() - v.getBottom() - chatListView.getPaddingBottom(); + break; + } + } + } + + chatListView.setPadding(0, p, 0, AndroidUtilities.dp(3)); + chatListView.setTopGlowOffset(contentPaddingTop + chatListViewPaddingTop); + + if (firstVisPos != RecyclerView.NO_POSITION && scrollToMessageObject != null) { + chatAdapter.updateRowsSafe(); + int index = messages.indexOf(scrollToMessageObject); + if (index >= 0) { + chatLayoutManager.scrollToPositionWithOffset(chatAdapter.messagesStartRow + index, top); + } + } + } + + } + + if (pinnedMessageView != null) { + pinnedMessageView.setTranslationY(contentPanTranslation + pinnedMessageEnterOffset + contentPaddingTop + topPanelViewH); + } + if (floatingDateView != null) { + floatingDateView.setTranslationY(contentPanTranslation + floatingDateViewOffset + viewsOffset + distanceTopViewOffset); + } + if (fragmentContextView != null) { + fragmentContextView.setTranslationY(contentPanTranslation + contentPaddingTop); + } + if (fragmentLocationContextView != null) { + fragmentLocationContextView.setTranslationY(contentPanTranslation + contentPaddingTop); + } + if (topChatPanelView != null) { + topChatPanelView.setTranslationY(contentPanTranslation + contentPaddingTop + topChatPanelViewOffset); + } + if (distanseTopView != null) { + distanseTopView.setTranslationY(contentPanTranslation + contentPaddingTop + AndroidUtilities.dp(50) + topChatPanelViewOffset); + } + } + private TextureView createTextureView(boolean add) { if (parentLayout == null) { @@ -6599,6 +6959,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return topChatPanelView != null && topChatPanelView.getTag() == null && reportSpamButton.getVisibility() != View.GONE; } + public void setChatInvite(TLRPC.ChatInvite invite) { + chatInvite = invite; + } + public void setBotUser(String value) { if (inlineReturn != 0) { getMessagesController().sendBotStart(currentUser, value); @@ -7682,7 +8046,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { allowGifs = currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(currentEncryptedChat.layer) >= 46; } - PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(0, allowGifs, true, ChatActivity.this); + PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(PhotoAlbumPickerActivity.SELECT_TYPE_ALL, allowGifs, true, ChatActivity.this); if (currentChat != null && !ChatObject.hasAdminRights(currentChat) && currentChat.slowmode_enabled) { fragment.setMaxSelectedPhotos(10, true); } else { @@ -7700,7 +8064,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not Intent videoPickerIntent = new Intent(); videoPickerIntent.setType("video/*"); videoPickerIntent.setAction(Intent.ACTION_GET_CONTENT); - videoPickerIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, (long) (1024 * 1024 * 1536)); + videoPickerIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, FileLoader.MAX_FILE_SIZE); Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType("image/*"); @@ -7734,7 +8098,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else if (Build.VERSION.SDK_INT >= 18) { takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(video)); } - takeVideoIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, (long) (1024 * 1024 * 1536)); + takeVideoIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, FileLoader.MAX_FILE_SIZE); currentPicturePath = video.getAbsolutePath(); } startActivityForResult(takeVideoIntent, 2); @@ -8102,8 +8466,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (messageObjectToEdit.canEditMedia()) { replyObjectTextView.setText(LocaleController.getString("EditMessageMedia", R.string.EditMessageMedia)); - } else if (messageObjectToEdit.messageText != null) { - String mess = messageObjectToEdit.messageText.toString(); + } else if (messageObjectToEdit.messageText != null || messageObjectToEdit.caption != null) { + String mess = messageObjectToEdit.caption != null ? messageObjectToEdit.caption.toString() : messageObjectToEdit.messageText.toString(); if (mess.length() > 150) { mess = mess.substring(0, 150); } @@ -8146,8 +8510,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (messageObjectToReply.messageOwner.media instanceof TLRPC.TL_messageMediaGame) { replyObjectTextView.setText(Emoji.replaceEmoji(messageObjectToReply.messageOwner.media.game.title, replyObjectTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(14), false)); - } else if (messageObjectToReply.messageText != null) { - String mess = messageObjectToReply.messageText.toString(); + } else if (messageObjectToReply.messageText != null || messageObjectToReply.caption != null) { + String mess = messageObjectToReply.caption != null ? messageObjectToReply.caption.toString() : messageObjectToReply.messageText.toString(); if (mess.length() > 150) { mess = mess.substring(0, 150); } @@ -8482,42 +8846,34 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatAdapter.notifyDataSetChanged(); } - private void scrollToLastMessage(boolean pagedown) { - if (chatListView.isScrollAnimationRunning()) { + private void scrollToLastMessage() { + if (chatListView.isFastScrollAnimationRunning()) { return; } chatScrollHelper.setScrollDirection(RecyclerAnimationScrollHelper.SCROLL_DIRECTION_DOWN); if (forwardEndReached[0] && first_unread_id == 0 && startLoadFromMessageId == 0) { - if (pagedown && chatLayoutManager.findFirstCompletelyVisibleItemPosition() == 0) { + if (chatLayoutManager.findFirstCompletelyVisibleItemPosition() == 0) { canShowPagedownButton = false; updatePagedownButtonVisibility(true); removeSelectedMessageHighlight(); updateVisibleRows(); } else { - int h = 0; - int maxH = (int) (chatListView.getMeasuredHeight() * 1.5f); - int i = chatLayoutManager.findFirstVisibleItemPosition() - 1; chatAdapter.updateRowsSafe(); - chatScrollHelperCallback.scrollTo = null; chatScrollHelper.scrollToPosition(0, 0, true, true); } } else { - if (pagedown) { - if (progressDialog != null) { - progressDialog.dismiss(); - } - progressDialog = new AlertDialog(getParentActivity(), 3); - progressDialog.setOnCancelListener(dialog -> postponedScrollIsCanceled = true); - progressDialog.showDelayed(400); - - postponedScrollToLastMessageQueryIndex = lastLoadIndex; - postponedScrollMessageId = 0; - postponedScrollIsCanceled = false; - waitingForLoad.clear(); - } else { - clearChatData(); + if (progressDialog != null) { + progressDialog.dismiss(); } + progressDialog = new AlertDialog(getParentActivity(), 3); + progressDialog.setOnCancelListener(dialog -> postponedScrollIsCanceled = true); + progressDialog.showDelayed(400); + + postponedScrollToLastMessageQueryIndex = lastLoadIndex; + postponedScrollMessageId = 0; + postponedScrollIsCanceled = false; + waitingForLoad.clear(); waitingForLoad.add(lastLoadIndex); getMessagesController().loadMessages(dialog_id, mergeDialogId, false, 30, 0, 0, true, 0, classGuid, 0, 0, ChatObject.isChannel(currentChat), inScheduleMode, lastLoadIndex++); @@ -8674,6 +9030,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } if (view.getBottom() <= chatListView.getPaddingTop() + AndroidUtilities.dp(1) + chatListViewClipTop) { + if (view instanceof ChatActionCell && messageObject.isDateObject) { + view.setAlpha(0f); + } continue; } int position = view.getBottom(); @@ -8684,7 +9043,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } minChild = view; } - if (chatListItemAniamtor == null || !chatListItemAniamtor.isRunning()) { + if (chatListItemAniamtor == null || !chatListItemAniamtor.willRemoved(view) && !chatListItemAniamtor.willAddedFromAlpha(view)) { if (view instanceof ChatActionCell && messageObject.isDateObject) { if (view.getAlpha() != 1.0f) { view.setAlpha(1.0f); @@ -8706,7 +9065,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { videoPlayerContainer.setTranslationY(-AndroidUtilities.roundMessageSize - 100); fragmentView.invalidate(); - if ((messageObject.isRoundVideo() || messageObject.isVideo()) && messageObject.eventId == 0 && checkTextureViewPosition && !chatListView.isScrollAnimationRunning()) { + if ((messageObject.isRoundVideo() || messageObject.isVideo()) && messageObject.eventId == 0 && checkTextureViewPosition && !chatListView.isFastScrollAnimationRunning()) { MediaController.getInstance().setCurrentVideoVisible(false); } } @@ -8750,14 +9109,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } float offset = minDateChild.getBottom() - chatListView.getPaddingTop() - chatListViewClipTop; if (offset > floatingDateView.getMeasuredHeight() && offset < floatingDateView.getMeasuredHeight() * 2) { - floatingDateView.setTranslationY(-floatingDateView.getMeasuredHeight() * 2 + offset); + floatingDateViewOffset = -floatingDateView.getMeasuredHeight() * 2 + offset; } else { - floatingDateView.setTranslationY(0); + floatingDateViewOffset = 0; } } else { hideFloatingDateView(true); - floatingDateView.setTranslationY(0); + floatingDateViewOffset = 0; } + updateChatListViewTopPadding(); if (!firstLoading && !paused && !inPreviewMode && !inScheduleMode) { int scheduledRead = 0; @@ -8858,7 +9218,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not getNotificationsController().updateServerNotificationsSettings(dialog_id); getNotificationsController().removeNotificationsForDialog(dialog_id); } else { - showDialog(AlertsCreator.createMuteAlert(getParentActivity(), dialog_id)); + BottomSheet alert = AlertsCreator.createMuteAlert(getParentActivity(), dialog_id); + alert.setCalcMandatoryInsets(isKeyboardVisible()); + showDialog(alert); } } else { SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); @@ -8910,7 +9272,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private AlertDialog progressDialog; public void scrollToMessageId(int id, int fromMessageId, boolean select, int loadIndex, boolean forceScroll) { - if (chatListView.isScrollAnimationRunning() || (chatListItemAniamtor != null && chatListItemAniamtor.isRunning()) || getParentActivity() == null) { + if (chatListView.isFastScrollAnimationRunning() || (chatListItemAniamtor != null && chatListItemAniamtor.isRunning()) || getParentActivity() == null) { return; } @@ -8999,77 +9361,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } if (!found) { - int h = 0; - int maxH = chatListView.getMeasuredHeight(); - int i; - View view; - if (scrollDirection == RecyclerAnimationScrollHelper.SCROLL_DIRECTION_DOWN) { - view = chatLayoutManager.findViewByPosition(chatLayoutManager.findFirstVisibleItemPosition()); - i = chatLayoutManager.findFirstVisibleItemPosition() - 1; - } else { - view = chatLayoutManager.findViewByPosition(chatLayoutManager.findLastVisibleItemPosition()); - i = chatLayoutManager.findLastVisibleItemPosition() + 1; - } - - - if (dummyMessageCell == null) { - dummyMessageCell = new ChatMessageCell(getParentActivity()); - } - - dummyMessageCell.isChat = currentChat != null || UserObject.isUserSelf(currentUser); - dummyMessageCell.isBot = currentUser != null && currentUser.bot; - dummyMessageCell.isMegagroup = ChatObject.isChannel(currentChat) && currentChat.megagroup; - - long lastGroupId = 0; - while (h < maxH) { - if (i < chatAdapter.messagesStartRow || i > chatAdapter.messagesEndRow) - break; - if (scrollDirection == RecyclerAnimationScrollHelper.SCROLL_DIRECTION_DOWN) { - if (position > i) break; - } else { - if (position < i) break; - } - MessageObject messageObject = messages.get(i - chatAdapter.messagesStartRow); - if (lastGroupId != 0 && messageObject.getGroupId() == lastGroupId) { - i += scrollDirection == RecyclerAnimationScrollHelper.SCROLL_DIRECTION_DOWN ? -1 : 1; - continue; - } - lastGroupId = messageObject.getGroupId(); - h += dummyMessageCell.computeHeight(messageObject, groupedMessagesMap.get(messageObject.getGroupId())); - i += scrollDirection == RecyclerAnimationScrollHelper.SCROLL_DIRECTION_DOWN ? -1 : 1; - } - - if (h < maxH) { - int yOffset = getScrollOffsetForMessage(object); - if (view != null) { - int scrollY; - if (scrollDirection == RecyclerAnimationScrollHelper.SCROLL_DIRECTION_UP) { - scrollY = view.getTop() - chatListView.getPaddingTop() - h - yOffset; - } else { - MessageObject messageObject = messages.get(position - chatAdapter.messagesStartRow); - int scrollToHeight = dummyMessageCell.computeHeight(messageObject, groupedMessagesMap.get(messageObject.getGroupId())); - int t = chatListView.getMeasuredHeight() - scrollToHeight; - scrollY = -(chatListView.getMeasuredHeight() - view.getBottom()) + t + h - yOffset; - } - int maxScrollOffset = chatListView.computeVerticalScrollRange() - chatListView.computeVerticalScrollOffset() - chatListView.computeVerticalScrollExtent(); - if (maxScrollOffset < 0) maxScrollOffset = 0; - if (scrollY > maxScrollOffset) { - scrollY = maxScrollOffset; - } - if (scrollY != 0) { - chatListView.smoothScrollBy(0, scrollY); - chatListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } - } else { - chatListView.smoothScrollToPosition(position); - } - } else { - int yOffset = getScrollOffsetForMessage(object); - chatScrollHelperCallback.scrollTo = object; - chatScrollHelperCallback.lastBottom = false; - chatScrollHelperCallback.lastItemOffset = yOffset; - chatScrollHelper.scrollToPosition(position, yOffset, false, true); - } + int yOffset = getScrollOffsetForMessage(object); + chatScrollHelperCallback.scrollTo = object; + chatScrollHelperCallback.lastBottom = false; + chatScrollHelperCallback.lastItemOffset = yOffset; + chatScrollHelper.scrollToPosition(position, yOffset, false, true); canShowPagedownButton = true; updatePagedownButtonVisibility(true); } @@ -9119,7 +9415,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } boolean show = canShowPagedownButton && !textSelectionHelper.isSelectionMode() && !chatActivityEnterView.isRecordingAudioVideo(); if (show) { - if (animated && SystemClock.elapsedRealtime() < openAnimationStartTime + 150) { + if (animated && (openAnimationStartTime == 0 || SystemClock.elapsedRealtime() < openAnimationStartTime + 150)) { animated = false; } pagedownButtonShowedByScroll = false; @@ -9935,7 +10231,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not PhotoViewer.getInstance().openPhotoForSelect(cameraPhoto, 0, 2, false, new PhotoViewer.EmptyPhotoViewerProvider() { @Override public ImageReceiver.BitmapHolder getThumbForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { - return new ImageReceiver.BitmapHolder(thumb, null); + return new ImageReceiver.BitmapHolder(thumb, null, 0); } @Override @@ -9950,7 +10246,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }, this); } else { fillEditingMediaWithCaption(caption, null); - SendMessagesHelper.prepareSendingVideo(getAccountInstance(), videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, null, null, 0, editingMessageObject, true, 0); + SendMessagesHelper.prepareSendingVideo(getAccountInstance(), videoPath, null, dialog_id, replyingMessageObject, null, null, 0, editingMessageObject, true, 0); afterMessageSend(); } } @@ -10318,6 +10614,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } firstLoading = false; AndroidUtilities.runOnUIThread(() -> { + getNotificationCenter().runDelayedNotifications(); if (parentLayout != null) { parentLayout.resumeDelayedFragmentAnimation(); } @@ -11068,6 +11365,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not replaceMessageObjects(arr, 0, true); } + boolean needMoveScrollToLastMessage = false; + boolean reloadMegagroup = false; if (!forwardEndReached[0]) { int currentMaxDate = Integer.MIN_VALUE; @@ -11104,14 +11403,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (a == 0 && obj.messageOwner.id < 0 && obj.type == MessageObject.TYPE_ROUND_VIDEO && !inScheduleMode) { needAnimateToMessage = obj; } - if (obj.isOut() && obj.isSending()) { - scrollToLastMessage(false); + if (obj.isOut() && obj.wasJustSent) { + scrollToLastMessage(); return; } if (obj.type < 0 || messagesDict[0].indexOfKey(messageId) >= 0) { continue; } - if (currentChat != null && currentChat.creator && (action instanceof TLRPC.TL_messageActionChatCreate || action instanceof TLRPC.TL_messageActionChatEditPhoto && messages.size() < 4)) { + if (currentChat != null && currentChat.creator && (!ChatObject.isChannel(currentChat) || currentChat.megagroup) && (action instanceof TLRPC.TL_messageActionChatCreate || action instanceof TLRPC.TL_messageActionChatEditPhoto && messages.size() < 2)) { continue; } if (action instanceof TLRPC.TL_messageActionChannelMigrateFrom) { @@ -11203,7 +11502,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (obj.type < 0 || messagesDict[0].indexOfKey(messageId) >= 0) { continue; } - if (currentChat != null && currentChat.creator && (action instanceof TLRPC.TL_messageActionChatCreate || action instanceof TLRPC.TL_messageActionChatEditPhoto && messages.size() < 4)) { + if (currentChat != null && currentChat.creator && (!ChatObject.isChannel(currentChat) || currentChat.megagroup) && (action instanceof TLRPC.TL_messageActionChatCreate || action instanceof TLRPC.TL_messageActionChatEditPhoto && messages.size() < 2)) { continue; } if (action instanceof TLRPC.TL_messageActionChannelMigrateFrom) { @@ -11300,6 +11599,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (obj.isOut() && !obj.messageOwner.from_scheduled) { removeUnreadPlane(true); + hideDistanceView(); hasFromMe = true; } @@ -11388,6 +11688,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } obj.stableId = lastStableId++; messages.add(placeToPaste, obj); + if (placeToPaste == 0) { + needMoveScrollToLastMessage = true; + } if (chatAdapter != null) { chatAdapter.notifyItemChanged(placeToPaste); chatAdapter.notifyItemInserted(placeToPaste); @@ -11475,9 +11778,23 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } if (inScheduleMode && !arr.isEmpty()) { - int mid = arr.get(0).getId(); + MessageObject messageObject = arr.get(0); + int mid = messageObject.getId(); if (mid < 0) { - scrollToMessageId(mid, 0, false, 0, true); + if (chatListItemAniamtor != null) { + chatListItemAniamtor.setShouldAnimateEnterFromBottom(needMoveScrollToLastMessage); + } + if (needMoveScrollToLastMessage) { + moveScrollToLastMessage(); + } else { + int index = messages.indexOf(messageObject); + if (chatLayoutManager != null && index > 0 && (chatLayoutManager.findViewByPosition(chatAdapter.messagesStartRow + index) != null || chatLayoutManager.findViewByPosition(chatAdapter.messagesStartRow + index - 1) != null)) { + chatLayoutManager.scrollToPositionWithOffset(chatAdapter.messagesStartRow + messages.indexOf(messageObject), getScrollOffsetForMessage(messageObject), false); + } else { + AndroidUtilities.runOnUIThread(() -> scrollToMessageId(mid, 0, false, 0, true)); + } + } + } } if (!messages.isEmpty() && botUser != null && botUser.length() == 0) { @@ -11714,7 +12031,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not scheduledMessagesCount--; updateScheduled = true; } - removeUnreadPlane(false); if (selectedMessagesIds[loadIndex].indexOfKey(ids) >= 0) { updatedSelected = true; addToSelectedMessages(obj, false, updatedSelectedLast = (a == size - 1)); @@ -11827,6 +12143,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not for (int a = 0, N = removedIndexes.size(); a < N; a++) { chatAdapter.notifyItemRemoved(removedIndexes.get(a)); } + removeUnreadPlane(false); + chatAdapter.notifyItemRangeChanged(chatAdapter.messagesStartRow, messages.size()); } updateVisibleRows(); } else { @@ -12670,6 +12988,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not long did = (Long) args[0]; if (did == dialog_id || currentUser != null && currentUser.id == did) { updateTopPanel(!paused); + updateDistanceView(true); } } else if (id == NotificationCenter.newDraftReceived) { long did = (Long) args[0]; @@ -13214,8 +13533,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int[] alowedNotifications = null; if (isOpen) { alowedNotifications = new int[]{ - NotificationCenter.chatInfoDidLoad, NotificationCenter.dialogsNeedReload, NotificationCenter.scheduledMessagesUpdated, NotificationCenter.pinnedMessageDidLoad, - NotificationCenter.closeChats, NotificationCenter.messagesDidLoad, NotificationCenter.botKeyboardDidLoad, NotificationCenter.userInfoDidLoad, NotificationCenter.needDeleteDialog/*, NotificationCenter.botInfoDidLoad*/ + NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, + NotificationCenter.botKeyboardDidLoad, NotificationCenter.needDeleteDialog }; openAnimationEnded = false; if (!backward) { @@ -13224,8 +13543,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { if (UserObject.isUserSelf(currentUser)) { alowedNotifications = new int[]{ - NotificationCenter.chatInfoDidLoad, NotificationCenter.dialogsNeedReload, NotificationCenter.scheduledMessagesUpdated, - NotificationCenter.closeChats, NotificationCenter.messagesDidLoad, NotificationCenter.botKeyboardDidLoad, NotificationCenter.userInfoDidLoad, NotificationCenter.needDeleteDialog, NotificationCenter.mediaDidLoad + NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.botKeyboardDidLoad, + NotificationCenter.needDeleteDialog, NotificationCenter.mediaDidLoad }; } if (chatActivityEnterView != null) { @@ -13610,7 +13929,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (animated) { pinnedMessageViewAnimator = new AnimatorSet(); - pinnedMessageViewAnimator.playTogether(ObjectAnimator.ofFloat(pinnedMessageView, View.TRANSLATION_Y, -AndroidUtilities.dp(50))); + ValueAnimator animator = ValueAnimator.ofFloat(pinnedMessageEnterOffset, -AndroidUtilities.dp(50)); + animator.addUpdateListener(animation -> { + pinnedMessageEnterOffset = (float) animation.getAnimatedValue(); + updateChatListViewTopPadding(); + }); + pinnedMessageViewAnimator.playTogether(animator); pinnedMessageViewAnimator.setDuration(200); pinnedMessageViewAnimator.addListener(new AnimatorListenerAdapter() { @Override @@ -13630,7 +13954,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); pinnedMessageViewAnimator.start(); } else { - pinnedMessageView.setTranslationY(-AndroidUtilities.dp(50)); + pinnedMessageEnterOffset = -AndroidUtilities.dp(50); pinnedMessageView.setVisibility(View.GONE); } return true; @@ -13681,7 +14005,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pinnedMessageViewAnimator = null; } if (animated) { - ValueAnimator animator = ValueAnimator.ofFloat(pinnedMessageView.getTranslationY(), 0); + ValueAnimator animator = ValueAnimator.ofFloat(pinnedMessageEnterOffset, 0); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { int position = -1; @@ -13701,7 +14025,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } } - pinnedMessageView.setTranslationY(translationY); + pinnedMessageEnterOffset = translationY; + updateChatListViewTopPadding(); + } }); pinnedMessageView.setVisibility(View.VISIBLE); @@ -13725,7 +14051,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); pinnedMessageViewAnimator.start(); } else { - pinnedMessageView.setTranslationY(0); + pinnedMessageEnterOffset = 0; + updateChatListViewTopPadding(); pinnedMessageView.setVisibility(View.VISIBLE); } } @@ -13846,6 +14173,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not boolean showReport = preferences.getBoolean("dialog_bar_report" + did, false); boolean showBlock = preferences.getBoolean("dialog_bar_block" + did, false); boolean showAdd = preferences.getBoolean("dialog_bar_add" + did, false); + boolean showArchive = preferences.getBoolean("dialog_bar_archived" + dialog_id, false); boolean showGeo = preferences.getBoolean("dialog_bar_location" + did, false); if (showReport || showBlock || showGeo) { @@ -13854,16 +14182,23 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not reportSpamButton.setVisibility(View.GONE); } + addToContactsButtonArchive = false; TLRPC.User user = currentUser != null ? getMessagesController().getUser(currentUser.id) : null; if (user != null) { if (!user.contact && !user.self && showAdd) { addContactItem.setVisibility(View.VISIBLE); - addToContactsButton.setVisibility(View.VISIBLE); addContactItem.setText(LocaleController.getString("AddToContacts", R.string.AddToContacts)); - if (reportSpamButton.getVisibility() == View.VISIBLE) { - addToContactsButton.setText(LocaleController.getString("AddContactChat", R.string.AddContactChat)); + addToContactsButton.setVisibility(View.VISIBLE); + if (showArchive) { + addToContactsButtonArchive = true; + addToContactsButton.setText(LocaleController.getString("Unarchive", R.string.Unarchive).toUpperCase()); + addToContactsButton.setTag(3); } else { - addToContactsButton.setText(LocaleController.formatString("AddContactFullChat", R.string.AddContactFullChat, UserObject.getFirstName(user)).toUpperCase()); + if (reportSpamButton.getVisibility() == View.VISIBLE) { + addToContactsButton.setText(LocaleController.getString("AddContactChat", R.string.AddContactChat)); + } else { + addToContactsButton.setText(LocaleController.formatString("AddContactFullChat", R.string.AddContactFullChat, UserObject.getFirstName(user)).toUpperCase()); + } } addToContactsButton.setTag(null); addToContactsButton.setVisibility(View.VISIBLE); @@ -13890,17 +14225,32 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not reportSpamButton.setText(LocaleController.getString("ReportSpamLocation", R.string.ReportSpamLocation)); reportSpamButton.setTag(R.id.object_tag, 1); reportSpamButton.setTextColor(Theme.getColor(Theme.key_chat_addContact)); + if (Build.VERSION.SDK_INT >= 21) { + Theme.setSelectorDrawableColor(reportSpamButton.getBackground(), Theme.getColor(Theme.key_chat_addContact) & 0x19ffffff, true); + } reportSpamButton.setTag(Theme.key_chat_addContact); } else { - reportSpamButton.setText(LocaleController.getString("ReportSpamAndLeave", R.string.ReportSpamAndLeave)); + if (showArchive) { + addToContactsButtonArchive = true; + addToContactsButton.setText(LocaleController.getString("Unarchive", R.string.Unarchive).toUpperCase()); + addToContactsButton.setTag(3); + addToContactsButton.setVisibility(View.VISIBLE); + reportSpamButton.setText(LocaleController.getString("ReportSpam", R.string.ReportSpam)); + } else { + addToContactsButton.setVisibility(View.GONE); + reportSpamButton.setText(LocaleController.getString("ReportSpamAndLeave", R.string.ReportSpamAndLeave)); + } reportSpamButton.setTag(R.id.object_tag, null); reportSpamButton.setTextColor(Theme.getColor(Theme.key_chat_reportSpam)); + if (Build.VERSION.SDK_INT >= 21) { + Theme.setSelectorDrawableColor(reportSpamButton.getBackground(), Theme.getColor(Theme.key_chat_reportSpam) & 0x19ffffff, true); + } reportSpamButton.setTag(Theme.key_chat_reportSpam); } + if (addContactItem != null) { addContactItem.setVisibility(View.GONE); } - addToContactsButton.setVisibility(View.GONE); } if (StrUtil.isBlank(NekoConfig.openPGPApp)) { @@ -13926,7 +14276,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (animated) { reportSpamViewAnimator = new AnimatorSet(); - reportSpamViewAnimator.playTogether(ObjectAnimator.ofFloat(topChatPanelView, View.TRANSLATION_Y, 0)); + ValueAnimator animator = ValueAnimator.ofFloat(topChatPanelViewOffset, 0); + animator.addUpdateListener(animation -> { + topChatPanelViewOffset = (float) animation.getAnimatedValue(); + updateChatListViewTopPadding(); + }); + reportSpamViewAnimator.playTogether(animator); reportSpamViewAnimator.setDuration(200); reportSpamViewAnimator.addListener(new AnimatorListenerAdapter() { @Override @@ -13945,7 +14300,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); reportSpamViewAnimator.start(); } else { - topChatPanelView.setTranslationY(0); + topChatPanelViewOffset = 0; + updateChatListViewTopPadding(); } } } else { @@ -13961,7 +14317,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (animated) { reportSpamViewAnimator = new AnimatorSet(); - reportSpamViewAnimator.playTogether(ObjectAnimator.ofFloat(topChatPanelView, View.TRANSLATION_Y, -AndroidUtilities.dp(50))); + ValueAnimator animator = ValueAnimator.ofFloat(topChatPanelViewOffset, -AndroidUtilities.dp(50)); + animator.addUpdateListener(animation -> { + topChatPanelViewOffset = (float) animation.getAnimatedValue(); + updateChatListViewTopPadding(); + }); + reportSpamViewAnimator.playTogether(animator); reportSpamViewAnimator.setDuration(200); reportSpamViewAnimator.addListener(new AnimatorListenerAdapter() { @Override @@ -13981,7 +14342,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); reportSpamViewAnimator.start(); } else { - topChatPanelView.setTranslationY(-AndroidUtilities.dp(50)); + topChatPanelViewOffset = -AndroidUtilities.dp(50); + updateChatListViewTopPadding(); } } } @@ -13993,41 +14355,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } try { - int firstVisPos = chatLayoutManager.findFirstVisibleItemPosition(); - int lastVisPos = chatLayoutManager.findLastVisibleItemPosition(); - int top = 0; - MessageObject scrollToMessageObject = null; - if (firstVisPos != RecyclerView.NO_POSITION) { - for (int i = firstVisPos; i <= lastVisPos; i++) { - View v = chatLayoutManager.findViewByPosition(i); - if (v instanceof ChatMessageCell) { - scrollToMessageObject = ((ChatMessageCell) v).getMessageObject(); - top = chatListView.getMeasuredHeight() - v.getBottom() - chatListView.getPaddingBottom(); - break; - } - } - } - if (chatListView.getPaddingTop() != AndroidUtilities.dp(52) && (pinnedMessageView != null && pinnedMessageView.getTag() == null || topChatPanelView != null && topChatPanelView.getTag() == null)) { - chatListView.setPadding(0, AndroidUtilities.dp(52), 0, AndroidUtilities.dp(3)); - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) floatingDateView.getLayoutParams(); - layoutParams.topMargin = AndroidUtilities.dp(52); - floatingDateView.setLayoutParams(layoutParams); - chatListView.setTopGlowOffset(AndroidUtilities.dp(48)); - } else if (chatListView.getPaddingTop() != AndroidUtilities.dp(4) && (pinnedMessageView == null || pinnedMessageView.getTag() != null) && (topChatPanelView == null || topChatPanelView.getTag() != null)) { - chatListView.setPadding(0, AndroidUtilities.dp(4), 0, AndroidUtilities.dp(3)); - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) floatingDateView.getLayoutParams(); - layoutParams.topMargin = AndroidUtilities.dp(4); - floatingDateView.setLayoutParams(layoutParams); - chatListView.setTopGlowOffset(0); - } else { - firstVisPos = RecyclerView.NO_POSITION; - } - if (firstVisPos != RecyclerView.NO_POSITION && scrollToMessageObject != null) { - chatAdapter.updateRowsSafe(); - int index = messages.indexOf(scrollToMessageObject); - if (index >= 0) { - chatLayoutManager.scrollToPositionWithOffset(chatAdapter.messagesStartRow + index, top); - } + if (chatListViewPaddingTop != AndroidUtilities.dp(48) && (pinnedMessageView != null && pinnedMessageView.getTag() == null || topChatPanelView != null && topChatPanelView.getTag() == null)) { + chatListViewPaddingTop = AndroidUtilities.dp(48); + updateChatListViewTopPadding(); + } else if (chatListViewPaddingTop != 0 && (pinnedMessageView == null || pinnedMessageView.getTag() != null) && (topChatPanelView == null || topChatPanelView.getTag() != null)) { + chatListViewPaddingTop = 0; + updateChatListViewTopPadding(); } } catch (Exception e) { FileLog.e(e); @@ -14067,14 +14400,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void dismissCurrentDialig() { + public void dismissCurrentDialog() { if (chatAttachAlert != null && visibleDialog == chatAttachAlert) { chatAttachAlert.getPhotoLayout().closeCamera(false); chatAttachAlert.dismissInternal(); chatAttachAlert.getPhotoLayout().hideCamera(true); return; } - super.dismissCurrentDialig(); + super.dismissCurrentDialog(); } @Override @@ -14187,6 +14520,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not scrollToTopOnResume = false; scrollToMessage = null; } + paused = false; pausedOnLastMessage = false; checkScrollForLoad(false); @@ -14356,6 +14690,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not topUndoView.hide(true, 0); } } + + if (chatListItemAniamtor != null) { + chatListItemAniamtor.endAnimations(); + } + if (chatScrollHelper != null) { + chatScrollHelper.cancel(); + } } private void applyDraftMaybe(boolean canClear) { @@ -14489,6 +14830,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int idx = messages.indexOf(messageObject); if (idx >= 0) { chatAdapter.notifyItemRangeChanged(idx + chatAdapter.messagesStartRow, groupedMessages.messages.size()); + chatListView.setItemAnimator(null); } } } @@ -14643,6 +14985,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } private void createDeleteMessagesAlert(final MessageObject finalSelectedObject, final MessageObject.GroupedMessages finalSelectedGroup, int loadParticipant) { + if (finalSelectedObject == null && (selectedMessagesIds[0].size() + selectedMessagesIds[1].size()) == 0) { + return; + } AlertsCreator.createDeleteMessagesAlert(this, currentUser, currentChat, currentEncryptedChat, chatInfo, mergeDialogId, finalSelectedObject, selectedMessagesIds, finalSelectedGroup, inScheduleMode, loadParticipant, () -> { hideActionMode(); updatePinnedMessageView(true); @@ -14837,22 +15182,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not options.add(23); icons.add(R.drawable.baseline_report_24); } - if (message.canDeleteMessage(inScheduleMode, currentChat)) { - items.add(LocaleController.getString("Delete", R.string.Delete)); - options.add(1); - icons.add(R.drawable.baseline_delete_24); - } } else { if (selectedObject.getId() > 0 && allowChatActions) { items.add(LocaleController.getString("Reply", R.string.Reply)); options.add(8); icons.add(R.drawable.baseline_reply_24); } - if (message.canDeleteMessage(inScheduleMode, currentChat)) { - items.add(LocaleController.getString("Delete", R.string.Delete)); - options.add(1); + } + if (message.canDeleteMessage(inScheduleMode, currentChat)) { + items.add(LocaleController.getString("Delete", R.string.Delete)); + options.add(1); icons.add(R.drawable.baseline_delete_24); - } } } else if (type == 20) { items.add(LocaleController.getString("Retry", R.string.Retry)); @@ -15440,9 +15780,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (chatActivityEnterView != null) { chatActivityEnterView.getEditField().setAllowDrawCursor(true); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - getParentActivity().getWindow().getDecorView().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); - } } }; scrimPopupWindow.setDismissAnimationDuration(220); @@ -15483,7 +15820,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not popupY = totalHeight - height - AndroidUtilities.dp(8); } } else { - popupY = AndroidUtilities.statusBarHeight; + popupY = inBubbleMode ? 0 : AndroidUtilities.statusBarHeight; } scrimPopupWindow.showAtLocation(chatListView, Gravity.LEFT | Gravity.TOP, popupX, popupY - getCurrentPanTranslationY()); chatListView.stopScroll(); @@ -15518,9 +15855,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (chatActivityEnterView != null) { chatActivityEnterView.getEditField().setAllowDrawCursor(false); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - getParentActivity().getWindow().getDecorView().setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - } return; } @@ -15554,7 +15888,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not views.add(stickersPanel); } actionBar.showActionMode(bottomMessagesActionContainer, null, views.toArray(new View[0]), new boolean[]{false, true, true}, chatListView, translationY); - if (getParentActivity() != null) { + if (getParentActivity() instanceof LaunchActivity) { ((LaunchActivity) getParentActivity()).hideVisibleActionMode(); } chatActivityEnterView.getEditField().setAllowDrawCursor(false); @@ -15657,6 +15991,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pattern.put(128, 1); pattern.put(140, 0); drawable.setVibrationPattern(pattern); + } else if (EmojiData.isPeachEmoji(emoji)) { + HashMap pattern = new HashMap<>(); + pattern.put(34, 1); + drawable.setVibrationPattern(pattern); } } } @@ -15955,7 +16293,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not break; } case 9: { - showDialog(new StickersAlert(getParentActivity(), this, selectedObject.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE && (currentChat == null || ChatObject.canSendStickers(currentChat)) ? chatActivityEnterView : null)); + StickersAlert alert = new StickersAlert(getParentActivity(), this, selectedObject.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE && (currentChat == null || ChatObject.canSendStickers(currentChat)) ? chatActivityEnterView : null); + alert.setCalcMandatoryInsets(isKeyboardVisible()); + showDialog(alert); break; } case 10: { @@ -16678,7 +17018,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (!cell.getMessageObject().deleted) { + cell.setIsUpdating(true); cell.setMessageObject(cell.getMessageObject(), cell.getCurrentMessagesGroup(), cell.isPinnedBottom(), cell.isPinnedTop()); + cell.setIsUpdating(false); } if (cell != scrimView) { cell.setCheckPressed(!disableSelection, disableSelection && selected); @@ -16863,9 +17205,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fillEditingMediaWithCaption(photoEntry.caption, photoEntry.entities); if (photoEntry.isVideo) { if (videoEditedInfo != null) { - SendMessagesHelper.prepareSendingVideo(getAccountInstance(), photoEntry.path, videoEditedInfo.estimatedSize, videoEditedInfo.estimatedDuration, videoEditedInfo.resultWidth, videoEditedInfo.resultHeight, videoEditedInfo, dialog_id, replyingMessageObject, photoEntry.caption, photoEntry.entities, photoEntry.ttl, editingMessageObject, notify, scheduleDate); + SendMessagesHelper.prepareSendingVideo(getAccountInstance(), photoEntry.path, videoEditedInfo, dialog_id, replyingMessageObject, photoEntry.caption, photoEntry.entities, photoEntry.ttl, editingMessageObject, notify, scheduleDate); } else { - SendMessagesHelper.prepareSendingVideo(getAccountInstance(), photoEntry.path, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, photoEntry.caption, photoEntry.entities, photoEntry.ttl, editingMessageObject, notify, scheduleDate); + SendMessagesHelper.prepareSendingVideo(getAccountInstance(), photoEntry.path, null, dialog_id, replyingMessageObject, photoEntry.caption, photoEntry.entities, photoEntry.ttl, editingMessageObject, notify, scheduleDate); } afterMessageSend(); } else { @@ -17121,7 +17463,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not webPage = null; } if (webPage != null) { - EmbedBottomSheet.show(getParentActivity(), webPage.site_name, webPage.title, webPage.url, webPage.embed_url, webPage.embed_width, webPage.embed_height, seekTime); + EmbedBottomSheet.show(getParentActivity(), webPage.site_name, webPage.title, webPage.url, webPage.embed_url, webPage.embed_width, webPage.embed_height, seekTime, isKeyboardVisible()); } else { if (!messageObject.isVideo() && messageObject.replyMessageObject != null) { MessageObject obj = messagesDict[messageObject.replyMessageObject.getDialogId() == dialog_id ? 0 : 1].get(messageObject.replyMessageObject.getId()); @@ -17243,7 +17585,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (messageObject != null && messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && messageObject.messageOwner.media.webpage != null && messageObject.messageOwner.media.webpage.cached_page != null) { String lowerUrl = urlFinal.toLowerCase(); String lowerUrl2 = messageObject.messageOwner.media.webpage.url.toLowerCase(); - if ((lowerUrl.contains("telegram.org/blog") || lowerUrl.contains("telegra.ph") || lowerUrl.contains("t.me/iv")) && (lowerUrl.contains(lowerUrl2) || lowerUrl2.contains(lowerUrl))) { + if ((lowerUrl.contains("telegram.org/blog") || Browser.isTelegraphUrl(lowerUrl, false) || lowerUrl.contains("t.me/iv")) && (lowerUrl.contains(lowerUrl2) || lowerUrl2.contains(lowerUrl))) { ArticleViewer.getInstance().setParentActivity(getParentActivity(), ChatActivity.this); ArticleViewer.getInstance().open(messageObject); return; @@ -17399,7 +17741,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not messagesStartRow = -1; messagesEndRow = -1; - if (currentUser != null && currentUser.bot && !MessagesController.isSupportUser(currentUser) && !inScheduleMode) { + if (currentUser != null && currentUser.bot && !MessagesController.isSupportUser(currentUser) && !inScheduleMode && (messages.isEmpty() || endReached[0])) { botInfoRow = rowCount++; } else { botInfoRow = -1; @@ -17540,7 +17882,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not MediaController.getInstance().setVoiceMessagesPlaylist(result ? createVoiceMessagesPlaylist(messageObject, false) : null, false); return result; } else if (messageObject.isMusic()) { - return MediaController.getInstance().setPlaylist(messages, messageObject); + return MediaController.getInstance().setPlaylist(messages, messageObject, mergeDialogId); } return false; } @@ -17684,7 +18026,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void needOpenWebView(String url, String title, String description, String originalUrl, int w, int h) { try { - EmbedBottomSheet.show(mContext, title, description, originalUrl, url, w, h); + EmbedBottomSheet.show(mContext, title, description, originalUrl, url, w, h, isKeyboardVisible()); } catch (Throwable e) { FileLog.e(e); } @@ -17744,7 +18086,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not SecretMediaViewer.getInstance().setParentActivity(getParentActivity()); SecretMediaViewer.getInstance().openMedia(message, photoViewerProvider); } else if (message.type == MessageObject.TYPE_STICKER || message.type == MessageObject.TYPE_ANIMATED_STICKER) { - showDialog(new StickersAlert(getParentActivity(), ChatActivity.this, message.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE && (currentChat == null || ChatObject.canSendStickers(currentChat)) ? chatActivityEnterView : null)); + StickersAlert alert = new StickersAlert(getParentActivity(), ChatActivity.this, message.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE && (currentChat == null || ChatObject.canSendStickers(currentChat)) ? chatActivityEnterView : null); + alert.setCalcMandatoryInsets(isKeyboardVisible()); + showDialog(alert); } else if (message.isVideo() || message.type == 1 || message.type == 0 && !message.isWebpageDocument() || message.isGif()) { openPhotoViewerForMessage(cell, message); } else if (message.type == 3) { @@ -17932,7 +18276,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatMessageCell.setAllowAssistant(true); } } else if (viewType == 1) { - view = new ChatActionCell(mContext); + view = new ChatActionCell(mContext) { + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + // if alpha == 0, then visibleToUser == false, so we need to override it + // to keep accessibility working correctly + info.setVisibleToUser(true); + } + }; ((ChatActionCell) view).setDelegate(new ChatActionCell.ChatActionCellDelegate() { @Override public void didClickImage(ChatActionCell cell) { @@ -18093,7 +18445,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } } - if (ChatObject.isChannel(currentChat) && currentChat.megagroup && message.messageOwner.from_id <= 0 && message.messageOwner.fwd_from != null && message.messageOwner.fwd_from.channel_post != 0) { + if (ChatObject.isChannel(currentChat) && currentChat.megagroup && message.messageOwner.from_id <= 0 && message.messageOwner.fwd_from != null && message.messageOwner.fwd_from.saved_from_peer instanceof TLRPC.TL_peerChannel) { if (!pinnedTopByGroup) { pinnedTop = false; } @@ -18198,6 +18550,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { messageCell.setVisibility(View.VISIBLE); } + if (!animatingDocuments.isEmpty() && animatingDocuments.containsKey(message.getDocument())) { + animatingDocuments.remove(message.getDocument()); + if (chatListItemAniamtor != null) { + chatListItemAniamtor.onGreetingStickerTransition(holder, greetingsViewContainer); + } + } } else if (view instanceof ChatActionCell) { ChatActionCell actionCell = (ChatActionCell) view; actionCell.setMessageObject(message); @@ -18559,6 +18917,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return false; } + public void setPreloadedSticker(TLRPC.Document preloadedSticker) { + preloadedGreetingsSticker = preloadedSticker; + } + public class ChatScrollCallback extends RecyclerAnimationScrollHelper.AnimationCallback { private MessageObject scrollTo; @@ -18586,8 +18948,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not checkTextureViewPosition = true; chatListView.getOnScrollListener().onScrolled(chatListView, 0, chatScrollHelper.getScrollDirection() == RecyclerAnimationScrollHelper.SCROLL_DIRECTION_DOWN ? 1 : -1); - getNotificationCenter().onAnimationFinish(animationIndex); updateVisibleRows(); + + AndroidUtilities.runOnUIThread(() -> getNotificationCenter().onAnimationFinish(animationIndex)); + } + + @Override + public void recycleView(View view) { + if (view instanceof ChatMessageCell) { + chatMessageCellsCache.add((ChatMessageCell) view); + } } } @@ -18612,7 +18982,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not View child = chatListView.getChildAt(a); if (child instanceof ChatMessageCell) { ((ChatMessageCell) child).createSelectorDrawable(); - } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatEditActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatEditActivity.java index 2e0762154..a41b10990 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatEditActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatEditActivity.java @@ -12,12 +12,14 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.InputFilter; @@ -30,7 +32,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.FrameLayout; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ScrollView; @@ -39,6 +40,7 @@ import org.telegram.messenger.ChatObject; import org.telegram.messenger.FileLog; import org.telegram.messenger.ImageLocation; import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.NotificationCenter; @@ -85,7 +87,6 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image private LinearLayout avatarContainer; private BackupImageView avatarImage; private View avatarOverlay; - private ImageView avatarEditor; private AnimatorSet avatarAnimation; private RadialProgressView avatarProgressView; private AvatarDrawable avatarDrawable; @@ -114,6 +115,7 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image private TextCell adminCell; private TextCell blockCell; private TextCell logCell; + private TextCell setAvatarCell; private ShadowSectionCell infoSectionCell; private FrameLayout deleteContainer; @@ -121,11 +123,9 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image private ShadowSectionCell deleteInfoCell; private TLRPC.FileLocation avatar; - private TLRPC.FileLocation avatarBig; private TLRPC.Chat currentChat; private TLRPC.ChatFull info; private int chatId; - private TLRPC.InputFile uploadedAvatar; private boolean signMessages; private boolean isChannel; @@ -139,10 +139,54 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image private int realAdminCount = 0; + private PhotoViewer.PhotoViewerProvider provider = new PhotoViewer.EmptyPhotoViewerProvider() { + + @Override + public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index, boolean needPreview) { + if (fileLocation == null) { + return null; + } + + TLRPC.FileLocation photoBig = null; + TLRPC.Chat chat = getMessagesController().getChat(chatId); + if (chat != null && chat.photo != null && chat.photo.photo_big != null) { + photoBig = chat.photo.photo_big; + } + + if (photoBig != null && photoBig.local_id == fileLocation.local_id && photoBig.volume_id == fileLocation.volume_id && photoBig.dc_id == fileLocation.dc_id) { + int[] coords = new int[2]; + avatarImage.getLocationInWindow(coords); + PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); + object.viewX = coords[0]; + object.viewY = coords[1] - (Build.VERSION.SDK_INT >= 21 ? 0 : AndroidUtilities.statusBarHeight); + object.parentView = avatarImage; + object.imageReceiver = avatarImage.getImageReceiver(); + object.dialogId = -chatId; + object.thumb = object.imageReceiver.getBitmapSafe(); + object.size = -1; + object.radius = avatarImage.getImageReceiver().getRoundRadius(); + object.scale = avatarContainer.getScaleX(); + object.canEdit = true; + return object; + } + return null; + } + + @Override + public void willHidePhotoViewer() { + avatarImage.getImageReceiver().setVisible(true, true); + } + + @Override + public void openPhotoForEdit(String file, String thumb, boolean isVideo) { + imageUpdater.openPhotoForEdit(file, thumb, 0, isVideo); + } + }; + public ChatEditActivity(Bundle args) { super(args); avatarDrawable = new AvatarDrawable(); - imageUpdater = new ImageUpdater(); + imageUpdater = new ImageUpdater(true); chatId = args.getInt("chat_id", 0); } @@ -164,11 +208,13 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image } } + avatarDrawable.setInfo(5, currentChat.title, null); isChannel = ChatObject.isChannel(currentChat) && !currentChat.megagroup; imageUpdater.parentFragment = this; - imageUpdater.delegate = this; + imageUpdater.setDelegate(this); signMessages = currentChat.signatures; NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.chatInfoDidLoad); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.updateInterfaces); return super.onFragmentCreate(); } @@ -179,6 +225,7 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image imageUpdater.clear(); } NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.chatInfoDidLoad); + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.updateInterfaces); AndroidUtilities.removeAdjustResize(getParentActivity(), classGuid, true); if (nameTextView != null) { nameTextView.onDestroy(); @@ -194,6 +241,7 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image } AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid, true); updateFields(true); + imageUpdater.onResume(); } @Override @@ -202,6 +250,25 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image if (nameTextView != null) { nameTextView.onPause(); } + imageUpdater.onPause(); + } + + @Override + public void dismissCurrentDialog() { + if (imageUpdater.dismissCurrentDialog(visibleDialog)) { + return; + } + super.dismissCurrentDialog(); + } + + @Override + public boolean dismissDialogOnPause(Dialog dialog) { + return imageUpdater.dismissDialogOnPause(dialog) && super.dismissDialogOnPause(dialog); + } + + @Override + public void onRequestPermissionsResultFragment(int requestCode, String[] permissions, int[] grantResults) { + imageUpdater.onRequestPermissionsResultFragment(requestCode, permissions, grantResults); } @Override @@ -395,10 +462,9 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image } }; avatarImage.setRoundRadius(AndroidUtilities.dp(32)); - frameLayout.addView(avatarImage, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 12)); if (ChatObject.canChangeChatInfo(currentChat)) { - avatarDrawable.setInfo(5, null, null); + frameLayout.addView(avatarImage, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 8)); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(0x55000000); @@ -408,48 +474,41 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image protected void onDraw(Canvas canvas) { if (avatarImage != null && avatarImage.getImageReceiver().hasNotThumb()) { paint.setAlpha((int) (0x55 * avatarImage.getImageReceiver().getCurrentAlpha())); - canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, AndroidUtilities.dp(32), paint); + canvas.drawCircle(getMeasuredWidth() / 2.0f, getMeasuredHeight() / 2.0f, getMeasuredWidth() / 2.0f, paint); } } }; - frameLayout.addView(avatarOverlay, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 12)); - avatarOverlay.setOnClickListener(view -> imageUpdater.openMenu(avatar != null, () -> { - avatar = null; - avatarBig = null; - uploadedAvatar = null; - showAvatarProgress(false, true); - avatarImage.setImage(null, null, avatarDrawable, currentChat); - })); - avatarOverlay.setContentDescription(LocaleController.getString("ChoosePhoto", R.string.ChoosePhoto)); - - avatarEditor = new ImageView(context) { - @Override - public void invalidate(int l, int t, int r, int b) { - super.invalidate(l, t, r, b); - avatarOverlay.invalidate(); - } - - @Override - public void invalidate() { - super.invalidate(); - avatarOverlay.invalidate(); - } - }; - avatarEditor.setScaleType(ImageView.ScaleType.CENTER); - avatarEditor.setImageResource(R.drawable.baseline_camera_alt_24); - avatarEditor.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_actionBarDefaultIcon), PorterDuff.Mode.SRC_IN)); - avatarEditor.setEnabled(false); - avatarEditor.setClickable(false); - frameLayout.addView(avatarEditor, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 12)); + frameLayout.addView(avatarOverlay, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 8)); avatarProgressView = new RadialProgressView(context); avatarProgressView.setSize(AndroidUtilities.dp(30)); avatarProgressView.setProgressColor(0xffffffff); - frameLayout.addView(avatarProgressView, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 12)); + avatarProgressView.setNoProgress(false); + frameLayout.addView(avatarProgressView, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 8)); showAvatarProgress(false, false); + + avatarContainer.setOnClickListener(v -> { + if (imageUpdater.isUploadingImage()) { + return; + } + TLRPC.Chat chat = getMessagesController().getChat(chatId); + if (chat.photo != null && chat.photo.photo_big != null) { + PhotoViewer.getInstance().setParentActivity(getParentActivity()); + if (chat.photo.dc_id != 0) { + chat.photo.photo_big.dc_id = chat.photo.dc_id; + } + ImageLocation videoLocation; + if (info != null && (info.chat_photo instanceof TLRPC.TL_photo) && !info.chat_photo.video_sizes.isEmpty()) { + videoLocation = ImageLocation.getForPhoto(info.chat_photo.video_sizes.get(0), info.chat_photo); + } else { + videoLocation = null; + } + PhotoViewer.getInstance().openPhotoWithVideo(chat.photo.photo_big, videoLocation, provider); + } + }); } else { - avatarDrawable.setInfo(5, currentChat.title, null); + frameLayout.addView(avatarImage, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 12, LocaleController.isRTL ? 16 : 0, 12)); } nameTextView = new EditTextEmoji(context, sizeNotifierFrameLayout, this, EditTextEmoji.STYLE_FRAGMENT); @@ -461,6 +520,25 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image } nameTextView.setEnabled(ChatObject.canChangeChatInfo(currentChat)); nameTextView.setFocusable(nameTextView.isEnabled()); + nameTextView.getEditText().addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + avatarDrawable.setInfo(5, nameTextView.getText().toString(), null); + if (avatarImage != null) { + avatarImage.invalidate(); + } + } + }); InputFilter[] inputFilters = new InputFilter[1]; inputFilters[0] = new InputFilter.LengthFilter(128); nameTextView.setFilters(inputFilters); @@ -471,6 +549,24 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image settingsContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); linearLayout1.addView(settingsContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + if (ChatObject.canChangeChatInfo(currentChat)) { + setAvatarCell = new TextCell(context) { + @Override + protected void onDraw(Canvas canvas) { + canvas.drawLine(LocaleController.isRTL ? 0 : AndroidUtilities.dp(20), getMeasuredHeight() - 1, getMeasuredWidth() - (LocaleController.isRTL ? AndroidUtilities.dp(20) : 0), getMeasuredHeight() - 1, Theme.dividerPaint); + } + }; + setAvatarCell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); + setAvatarCell.setColors(Theme.key_windowBackgroundWhiteBlueIcon, Theme.key_windowBackgroundWhiteBlueButton); + setAvatarCell.setOnClickListener(v -> imageUpdater.openMenu(avatar != null, () -> { + avatar = null; + MessagesController.getInstance(currentAccount).changeChatAvatar(chatId, null, null, null, 0, null, null, null); + showAvatarProgress(false, true); + avatarImage.setImage(null, null, avatarDrawable, currentChat); + })); + settingsContainer.addView(setAvatarCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); + } + descriptionTextView = new EditTextBoldCursor(context); descriptionTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); descriptionTextView.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)); @@ -489,7 +585,11 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image descriptionTextView.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); descriptionTextView.setCursorSize(AndroidUtilities.dp(20)); descriptionTextView.setCursorWidth(1.5f); - settingsContainer.addView(descriptionTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 23, 12, 23, 6)); + if (descriptionTextView.isEnabled()) { + settingsContainer.addView(descriptionTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 23, 15, 23, 9)); + } else { + settingsContainer.addView(descriptionTextView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 23, 12, 23, 6)); + } descriptionTextView.setOnEditorActionListener((textView, i, keyEvent) -> { if (i == EditorInfo.IME_ACTION_DONE && doneButton != null) { doneButton.performClick(); @@ -780,19 +880,43 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image if (info != null) { descriptionTextView.setText(info.about); } - if (currentChat.photo != null) { - avatar = currentChat.photo.photo_small; - avatarBig = currentChat.photo.photo_big; - avatarImage.setImage(ImageLocation.getForChat(currentChat, false), "50_50", avatarDrawable, currentChat); - } else { - avatarImage.setImageDrawable(avatarDrawable); - } - + setAvatar(); updateFields(true); return fragmentView; } + private void setAvatar() { + if (avatarImage == null) { + return; + } + TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(chatId); + if (chat == null) { + return; + } + currentChat = chat; + boolean hasPhoto; + if (currentChat.photo != null) { + avatar = currentChat.photo.photo_small; + ImageLocation location = ImageLocation.getForChat(currentChat, false); + avatarImage.setImage(location, "50_50", avatarDrawable, currentChat); + hasPhoto = location != null; + } else { + avatarImage.setImageDrawable(avatarDrawable); + hasPhoto = false; + } + if (setAvatarCell != null) { + if (hasPhoto || imageUpdater.isUploadingImage()) { + setAvatarCell.setTextAndIcon(LocaleController.getString("ChatSetNewPhoto", R.string.ChatSetNewPhoto), R.drawable.menu_camera2, true); + } else { + setAvatarCell.setTextAndIcon(LocaleController.getString("ChatSetPhotoOrVideo", R.string.ChatSetPhotoOrVideo), R.drawable.menu_camera2, true); + } + } + if (PhotoViewer.hasInstance() && PhotoViewer.getInstance().isVisible()) { + PhotoViewer.getInstance().checkCurrentImageVisibility(); + } + } + @Override public void didReceivedNotification(int id, int account, Object... args) { if (id == NotificationCenter.chatInfoDidLoad) { @@ -805,14 +929,36 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image historyHidden = !ChatObject.isChannel(currentChat) || info.hidden_prehistory; updateFields(false); } + } else if (id == NotificationCenter.updateInterfaces) { + int mask = (Integer) args[0]; + if ((mask & MessagesController.UPDATE_MASK_AVATAR) != 0) { + setAvatar(); + } } } @Override - public void didUploadPhoto(final TLRPC.InputFile file, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { + public void onUploadProgressChanged(float progress) { + if (avatarProgressView == null) { + return; + } + avatarProgressView.setProgress(progress); + } + + @Override + public void didStartUpload(boolean isVideo) { + if (avatarProgressView == null) { + return; + } + avatarProgressView.setProgress(0.0f); + } + + @Override + public void didUploadPhoto(final TLRPC.InputFile photo, final TLRPC.InputFile video, double videoStartTimestamp, String videoPath, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { AndroidUtilities.runOnUIThread(() -> { - if (file != null) { - uploadedAvatar = file; + avatar = smallSize.location; + if (photo != null || video != null) { + MessagesController.getInstance(currentAccount).changeChatAvatar(chatId, null, photo, video, videoStartTimestamp, videoPath, smallSize.location, bigSize.location); if (createAfterUpload) { try { if (progressDialog != null && progressDialog.isShowing()) { @@ -827,9 +973,8 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image } showAvatarProgress(false, true); } else { - avatar = smallSize.location; - avatarBig = bigSize.location; avatarImage.setImage(ImageLocation.getForLocal(avatar), "50_50", avatarDrawable, currentChat); + setAvatarCell.setTextAndIcon(LocaleController.getString("ChatSetNewPhoto", R.string.ChatSetNewPhoto), R.drawable.menu_camera2, true); showAvatarProgress(true, false); } }); @@ -843,11 +988,9 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image private boolean checkDiscard() { String about = info != null && info.about != null ? info.about : ""; if (info != null && ChatObject.isChannel(currentChat) && info.hidden_prehistory != historyHidden || - imageUpdater.uploadingImage != null || nameTextView != null && !currentChat.title.equals(nameTextView.getText().toString()) || descriptionTextView != null && !about.equals(descriptionTextView.getText().toString()) || - signMessages != currentChat.signatures || - uploadedAvatar != null || avatar == null && currentChat.photo instanceof TLRPC.TL_chatPhoto) { + signMessages != currentChat.signatures) { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setTitle(LocaleController.getString("UserRestrictionsApplyChanges", R.string.UserRestrictionsApplyChanges)); if (isChannel) { @@ -940,7 +1083,7 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image } } - if (imageUpdater.uploadingImage != null) { + if (imageUpdater.isUploadingImage()) { createAfterUpload = true; progressDialog = new AlertDialog(getParentActivity(), 3); progressDialog.setOnCancelListener(dialog -> { @@ -963,16 +1106,11 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image currentChat.signatures = true; MessagesController.getInstance(currentAccount).toogleChannelSignatures(chatId, signMessages); } - if (uploadedAvatar != null) { - MessagesController.getInstance(currentAccount).changeChatAvatar(chatId, uploadedAvatar, avatar, avatarBig); - } else if (avatar == null && currentChat.photo instanceof TLRPC.TL_chatPhoto) { - MessagesController.getInstance(currentAccount).changeChatAvatar(chatId, null, null, null); - } finishFragment(); } private void showAvatarProgress(boolean show, boolean animated) { - if (avatarEditor == null) { + if (avatarProgressView == null) { return; } if (avatarAnimation != null) { @@ -983,26 +1121,23 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image avatarAnimation = new AnimatorSet(); if (show) { avatarProgressView.setVisibility(View.VISIBLE); - - avatarAnimation.playTogether(ObjectAnimator.ofFloat(avatarEditor, View.ALPHA, 0.0f), - ObjectAnimator.ofFloat(avatarProgressView, View.ALPHA, 1.0f)); + avatarOverlay.setVisibility(View.VISIBLE); + avatarAnimation.playTogether(ObjectAnimator.ofFloat(avatarProgressView, View.ALPHA, 1.0f), + ObjectAnimator.ofFloat(avatarOverlay, View.ALPHA, 1.0f)); } else { - avatarEditor.setVisibility(View.VISIBLE); - - avatarAnimation.playTogether(ObjectAnimator.ofFloat(avatarEditor, View.ALPHA, 1.0f), - ObjectAnimator.ofFloat(avatarProgressView, View.ALPHA, 0.0f)); + avatarAnimation.playTogether(ObjectAnimator.ofFloat(avatarProgressView, View.ALPHA, 0.0f), + ObjectAnimator.ofFloat(avatarOverlay, View.ALPHA, 0.0f)); } avatarAnimation.setDuration(180); avatarAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - if (avatarAnimation == null || avatarEditor == null) { + if (avatarAnimation == null || avatarProgressView == null) { return; } - if (show) { - avatarEditor.setVisibility(View.INVISIBLE); - } else { + if (!show) { avatarProgressView.setVisibility(View.INVISIBLE); + avatarOverlay.setVisibility(View.INVISIBLE); } avatarAnimation = null; } @@ -1015,15 +1150,15 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image avatarAnimation.start(); } else { if (show) { - avatarEditor.setAlpha(1.0f); - avatarEditor.setVisibility(View.INVISIBLE); avatarProgressView.setAlpha(1.0f); avatarProgressView.setVisibility(View.VISIBLE); + avatarOverlay.setAlpha(1.0f); + avatarOverlay.setVisibility(View.VISIBLE); } else { - avatarEditor.setAlpha(1.0f); - avatarEditor.setVisibility(View.VISIBLE); avatarProgressView.setAlpha(0.0f); avatarProgressView.setVisibility(View.INVISIBLE); + avatarOverlay.setAlpha(0.0f); + avatarOverlay.setVisibility(View.INVISIBLE); } } } @@ -1251,7 +1386,6 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image ThemeDescription.ThemeDescriptionDelegate cellDelegate = () -> { if (avatarImage != null) { - avatarDrawable.setInfo(5, null, null); avatarImage.invalidate(); } }; @@ -1263,6 +1397,9 @@ public class ChatEditActivity extends BaseFragment implements ImageUpdater.Image themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle)); themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector)); + themeDescriptions.add(new ThemeDescription(setAvatarCell, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector)); + themeDescriptions.add(new ThemeDescription(setAvatarCell, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{TextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueButton)); + themeDescriptions.add(new ThemeDescription(setAvatarCell, 0, new Class[]{TextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueIcon)); themeDescriptions.add(new ThemeDescription(membersCell, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector)); themeDescriptions.add(new ThemeDescription(membersCell, ThemeDescription.FLAG_TEXTCOLOR, new Class[]{TextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); themeDescriptions.add(new ThemeDescription(membersCell, 0, new Class[]{TextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatUsersActivity.java index cef5cd15a..667c126aa 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatUsersActivity.java @@ -25,6 +25,7 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; @@ -53,6 +54,7 @@ import org.telegram.ui.ActionBar.ThemeDescription; import org.telegram.ui.Adapters.SearchAdapterHelper; import org.telegram.ui.Cells.GraySectionCell; import org.telegram.ui.Cells.HeaderCell; +import org.telegram.ui.Cells.LoadingCell; import org.telegram.ui.Cells.ManageChatTextCell; import org.telegram.ui.Cells.ShadowSectionCell; import org.telegram.ui.Cells.TextCheckCell2; @@ -60,8 +62,10 @@ import org.telegram.ui.Cells.TextInfoPrivacyCell; import org.telegram.ui.Cells.ManageChatUserCell; import org.telegram.ui.Cells.TextSettingsCell; import org.telegram.ui.Components.EmptyTextProgressView; +import org.telegram.ui.Components.IntSeekBarAccessibilityDelegate; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RecyclerListView; +import org.telegram.ui.Components.SeekBarAccessibilityDelegate; import org.telegram.ui.Components.UndoView; import java.util.ArrayList; @@ -69,6 +73,7 @@ import java.util.Collections; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; import tw.nekomimi.nekogram.NekoConfig; @@ -134,6 +139,7 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente private int botStartRow; private int botEndRow; private int membersHeaderRow; + private int loadingProgressRow; private int participantsInfoRow; private int blockedEmptyRow; @@ -166,8 +172,9 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente private class ChooseView extends View { - private Paint paint; - private TextPaint textPaint; + private final Paint paint; + private final TextPaint textPaint; + private final SeekBarAccessibilityDelegate accessibilityDelegate; private int circleSize; private int gapSize; @@ -219,6 +226,43 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente strings.add(string); sizes.add((int) Math.ceil(textPaint.measureText(string))); } + + accessibilityDelegate = new IntSeekBarAccessibilityDelegate() { + @Override + public int getProgress() { + return selectedSlowmode; + } + + @Override + public void setProgress(int progress) { + setItem(progress); + } + + @Override + public int getMaxValue() { + return strings.size() - 1; + } + + @Override + protected CharSequence getContentDescription(View host) { + if (selectedSlowmode == 0) { + return LocaleController.getString("SlowmodeOff", R.string.SlowmodeOff); + } else { + return formatSeconds(getSecondsForIndex(selectedSlowmode)); + } + } + }; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + accessibilityDelegate.onInitializeAccessibilityNodeInfoInternal(this, info); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + return super.performAccessibilityAction(action, arguments) || accessibilityDelegate.performAccessibilityActionInternal(this, action, arguments); } @Override @@ -280,19 +324,13 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente return; } selectedSlowmode = index; - for (int a = 0; a < 3; a++) { - RecyclerView.ViewHolder holder = listView.findViewHolderForAdapterPosition(slowmodeInfoRow); - if (holder != null) { - listViewAdapter.onBindViewHolder(holder, slowmodeInfoRow); - } - } + listViewAdapter.notifyItemChanged(slowmodeInfoRow); invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(74), MeasureSpec.EXACTLY)); - int width = MeasureSpec.getSize(widthMeasureSpec); circleSize = AndroidUtilities.dp(6); gapSize = AndroidUtilities.dp(2); sideSide = AndroidUtilities.dp(22); @@ -398,6 +436,7 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente slowmodeRow = -1; slowmodeSelectRow = -1; slowmodeInfoRow = -1; + loadingProgressRow = -1; rowCount = 0; if (type == TYPE_KICKED) { @@ -429,13 +468,18 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente if (ChatObject.canBlockUsers(currentChat)) { addNewRow = rowCount++; } - if (!participants.isEmpty()) { - participantsStartRow = rowCount; - rowCount += participants.size(); - participantsEndRow = rowCount; - } - if (addNewRow != -1 || participantsStartRow != -1) { - addNewSectionRow = rowCount++; + + if (loadingUsers && !firstLoaded) { + loadingProgressRow = rowCount++; + } else { + if (!participants.isEmpty()) { + participantsStartRow = rowCount; + rowCount += participants.size(); + participantsEndRow = rowCount; + } + if (addNewRow != -1 || participantsStartRow != -1) { + addNewSectionRow = rowCount++; + } } } else if (type == TYPE_BANNED) { if (ChatObject.canBlockUsers(currentChat)) { @@ -444,20 +488,24 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente participantsInfoRow = rowCount++; } } - if (!participants.isEmpty()) { - restricted1SectionRow = rowCount++; - participantsStartRow = rowCount; - rowCount += participants.size(); - participantsEndRow = rowCount; - } - if (participantsStartRow != -1) { - if (participantsInfoRow == -1) { - participantsInfoRow = rowCount++; - } else { - addNewSectionRow = rowCount++; - } + if (loadingUsers && !firstLoaded) { + loadingProgressRow = rowCount++; } else { - blockedEmptyRow = rowCount++; + if (!participants.isEmpty()) { + restricted1SectionRow = rowCount++; + participantsStartRow = rowCount; + rowCount += participants.size(); + participantsEndRow = rowCount; + } + if (participantsStartRow != -1) { + if (participantsInfoRow == -1) { + participantsInfoRow = rowCount++; + } else { + addNewSectionRow = rowCount++; + } + } else { + blockedEmptyRow = rowCount++; + } } } else if (type == TYPE_ADMIN) { if (ChatObject.isChannel(currentChat) && currentChat.megagroup && (info == null || info.participants_count <= 200) @@ -465,15 +513,20 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente recentActionsRow = rowCount++; addNewSectionRow = rowCount++; } + if (ChatObject.canAddAdmins(currentChat)) { addNewRow = rowCount++; } - if (!participants.isEmpty()) { - participantsStartRow = rowCount; - rowCount += participants.size(); - participantsEndRow = rowCount; + if (loadingUsers && !firstLoaded) { + loadingProgressRow = rowCount++; + } else { + if (!participants.isEmpty()) { + participantsStartRow = rowCount; + rowCount += participants.size(); + participantsEndRow = rowCount; + } + participantsInfoRow = rowCount++; } - participantsInfoRow = rowCount++; } else if (type == TYPE_USERS) { if (selectType == 0 && ChatObject.canAddUsers(currentChat)) { /*if (ChatObject.canUserDoAdminAction(currentChat, ChatObject.ACTION_INVITE) && (!ChatObject.isChannel(currentChat) || currentChat.megagroup || TextUtils.isEmpty(currentChat.username))) { @@ -482,31 +535,35 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente }*/ addNewRow = rowCount++; } - boolean hasAnyOther = false; - if (!contacts.isEmpty()) { - contactsHeaderRow = rowCount++; - contactsStartRow = rowCount; - rowCount += contacts.size(); - contactsEndRow = rowCount; - hasAnyOther = true; - } - if (!bots.isEmpty()) { - botHeaderRow = rowCount++; - botStartRow = rowCount; - rowCount += bots.size(); - botEndRow = rowCount; - hasAnyOther = true; - } - if (!participants.isEmpty()) { - if (hasAnyOther) { - membersHeaderRow = rowCount++; + if (loadingUsers && !firstLoaded) { + loadingProgressRow = rowCount++; + } else { + boolean hasAnyOther = false; + if (!contacts.isEmpty()) { + contactsHeaderRow = rowCount++; + contactsStartRow = rowCount; + rowCount += contacts.size(); + contactsEndRow = rowCount; + hasAnyOther = true; + } + if (!bots.isEmpty()) { + botHeaderRow = rowCount++; + botStartRow = rowCount; + rowCount += bots.size(); + botEndRow = rowCount; + hasAnyOther = true; + } + if (!participants.isEmpty()) { + if (hasAnyOther) { + membersHeaderRow = rowCount++; + } + participantsStartRow = rowCount; + rowCount += participants.size(); + participantsEndRow = rowCount; + } + if (rowCount != 0) { + participantsInfoRow = rowCount++; } - participantsStartRow = rowCount; - rowCount += participants.size(); - participantsEndRow = rowCount; - } - if (rowCount != 0) { - participantsInfoRow = rowCount++; } } } @@ -622,11 +679,12 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente emptyView.setText(LocaleController.getString("NoResult", R.string.NoResult)); } emptyView.setShowAtCenter(true); + emptyView.setVisibility(View.GONE); frameLayout.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView = new RecyclerListView(context); - listView.setEmptyView(emptyView); listView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); + ((SimpleItemAnimator) listView.getItemAnimator()).setSupportsChangeAnimations(false); listView.setAdapter(listViewAdapter = new ListAdapter(context)); listView.setVerticalScrollbarPosition(LocaleController.isRTL ? RecyclerListView.SCROLLBAR_POSITION_LEFT : RecyclerListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); @@ -853,9 +911,6 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente participant = null; } if (participant instanceof TLRPC.ChannelParticipant) { - if (participant instanceof TLRPC.TL_channelParticipantCreator) { - return; - } TLRPC.ChannelParticipant channelParticipant = (TLRPC.ChannelParticipant) participant; user_id = channelParticipant.user_id; canEditAdmin = !(channelParticipant instanceof TLRPC.TL_channelParticipantAdmin || channelParticipant instanceof TLRPC.TL_channelParticipantCreator) || channelParticipant.can_edit; @@ -863,9 +918,6 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente adminRights = channelParticipant.admin_rights; rank = channelParticipant.rank; } else if (participant instanceof TLRPC.ChatParticipant) { - if (participant instanceof TLRPC.TL_chatParticipantCreator) { - return; - } TLRPC.ChatParticipant chatParticipant = (TLRPC.ChatParticipant) participant; user_id = chatParticipant.user_id; canEditAdmin = currentChat.creator; @@ -977,6 +1029,8 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente } updateRows(); + listView.setEmptyView(emptyView); + if (needOpenSearch) { searchItem.openSearch(false); } @@ -1336,13 +1390,11 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente actions.add(1); } items.add(LocaleController.getString("KickFromGroup", R.string.KickFromGroup)); - icons.add(R.drawable.baseline_remove_circle_24); - actions.add(2); } else { items.add(LocaleController.getString("ChannelRemoveUser", R.string.ChannelRemoveUser)); - icons.add(R.drawable.baseline_remove_circle_24); - actions.add(2); } + icons.add(R.drawable.baseline_remove_circle_24); + actions.add(2); hasRemove = true; } if (actions == null || actions.isEmpty()) { @@ -1569,6 +1621,16 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente return 0; } + private String formatSeconds(int seconds) { + if (seconds < 60) { + return LocaleController.formatPluralString("Seconds", seconds); + } else if (seconds < 60 * 60) { + return LocaleController.formatPluralString("Minutes", seconds / 60); + } else { + return LocaleController.formatPluralString("Hours", seconds / 60 / 60); + } + } + private boolean checkDiscard() { String newBannedRights = ChatObject.getBannedRightsString(defaultBannedRights); if (!newBannedRights.equals(initialBannedRights) || initialSlowmode != selectedSlowmode) { @@ -1691,6 +1753,11 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente } } + @Override + public boolean needDelayOpenAnimation() { + return true; + } + private int getChannelAdminParticipantType(TLObject participant) { if (participant instanceof TLRPC.TL_channelParticipantCreator || participant instanceof TLRPC.TL_channelParticipantSelf) { return 0; @@ -1819,6 +1886,9 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente req.offset = offset; req.limit = count; int reqId = getConnectionsManager().sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> { + if (parentLayout != null) { + parentLayout.resumeDelayedFragmentAnimation(); + } if (error == null) { TLRPC.TL_channels_channelParticipants res = (TLRPC.TL_channels_channelParticipants) response; if (type == TYPE_ADMIN) { @@ -2439,7 +2509,7 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente @Override public int getItemCount() { - if (loadingUsers && !firstLoaded) { + if (type == TYPE_KICKED && loadingUsers && !firstLoaded) { return 0; } return rowCount; @@ -2525,6 +2595,9 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente case 8: view = new GraySectionCell(mContext); break; + case 10: + view = new LoadingCell(mContext,AndroidUtilities.dp(40), AndroidUtilities.dp(120)); + break; case 9: default: view = new ChooseView(mContext); @@ -2543,10 +2616,13 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente TLObject item = getItem(position); int lastRow; + boolean showJoined = false; if (position >= participantsStartRow && position < participantsEndRow) { lastRow = participantsEndRow; + showJoined = ChatObject.isChannel(currentChat) && !currentChat.megagroup; } else if (position >= contactsStartRow && position < contactsEndRow) { lastRow = contactsEndRow; + showJoined = ChatObject.isChannel(currentChat) && !currentChat.megagroup; } else { lastRow = botEndRow; } @@ -2558,35 +2634,27 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente boolean banned; boolean creator; boolean admin; - String joinDate; + int joined; if (item instanceof TLRPC.ChannelParticipant) { TLRPC.ChannelParticipant participant = (TLRPC.ChannelParticipant) item; userId = participant.user_id; kickedBy = participant.kicked_by; promotedBy = participant.promoted_by; bannedRights = participant.banned_rights; + joined = participant.date; banned = participant instanceof TLRPC.TL_channelParticipantBanned; creator = participant instanceof TLRPC.TL_channelParticipantCreator; admin = participant instanceof TLRPC.TL_channelParticipantAdmin; - if (creator) { - joinDate = LocaleController.getString("ChannelCreator", R.string.ChannelCreator); - } else { - joinDate = LocaleController.formatDateJoined(participant.date); - } } else { TLRPC.ChatParticipant participant = (TLRPC.ChatParticipant) item; userId = participant.user_id; + joined = participant.date; kickedBy = 0; promotedBy = 0; bannedRights = null; banned = false; creator = participant instanceof TLRPC.TL_chatParticipantCreator; admin = participant instanceof TLRPC.TL_chatParticipantAdmin; - if (creator) { - joinDate = LocaleController.getString("ChannelCreator", R.string.ChannelCreator); - } else { - joinDate = LocaleController.formatDateJoined(participant.date); - } } TLRPC.User user = getMessagesController().getUser(userId); if (user != null) { @@ -2617,7 +2685,13 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente } userCell.setData(user, null, role, position != lastRow - 1); } else if (type == TYPE_USERS) { - userCell.setData(user, null, joinDate, position != lastRow - 1); + CharSequence status; + if (showJoined && joined != 0) { + status = LocaleController.formatJoined(joined); + } else { + status = null; + } + userCell.setData(user, null, status, position != lastRow - 1); } } break; @@ -2625,18 +2699,10 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente TextInfoPrivacyCell privacyCell = (TextInfoPrivacyCell) holder.itemView; if (position == participantsInfoRow) { if (type == TYPE_BANNED || type == TYPE_KICKED) { - if (ChatObject.canBlockUsers(currentChat)) { - if (isChannel) { - privacyCell.setText(LocaleController.getString("NoBlockedChannel2", R.string.NoBlockedChannel2)); - } else { - privacyCell.setText(LocaleController.getString("NoBlockedGroup2", R.string.NoBlockedGroup2)); - } + if (isChannel) { + privacyCell.setText(LocaleController.getString("NoBlockedChannel2", R.string.NoBlockedChannel2)); } else { - if (isChannel) { - privacyCell.setText(LocaleController.getString("NoBlockedChannel2", R.string.NoBlockedChannel2)); - } else { - privacyCell.setText(LocaleController.getString("NoBlockedGroup2", R.string.NoBlockedGroup2)); - } + privacyCell.setText(LocaleController.getString("NoBlockedGroup2", R.string.NoBlockedGroup2)); } privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); } else if (type == TYPE_ADMIN) { @@ -2646,11 +2712,10 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente } else { privacyCell.setText(LocaleController.getString("MegaAdminsInfo", R.string.MegaAdminsInfo)); } - privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); } else { privacyCell.setText(""); - privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); } + privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); } else if (type == TYPE_USERS) { if (!isChannel || selectType != 0) { privacyCell.setText(""); @@ -2665,13 +2730,7 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente if (info == null || seconds == 0) { privacyCell.setText(LocaleController.getString("SlowmodeInfoOff", R.string.SlowmodeInfoOff)); } else { - if (seconds < 60) { - privacyCell.setText(LocaleController.formatString("SlowmodeInfoSelected", R.string.SlowmodeInfoSelected, LocaleController.formatPluralString("Seconds", seconds))); - } else if (seconds < 60 * 60) { - privacyCell.setText(LocaleController.formatString("SlowmodeInfoSelected", R.string.SlowmodeInfoSelected, LocaleController.formatPluralString("Minutes", seconds / 60))); - } else { - privacyCell.setText(LocaleController.formatString("SlowmodeInfoSelected", R.string.SlowmodeInfoSelected, LocaleController.formatPluralString("Hours", seconds / 60 / 60))); - } + privacyCell.setText(LocaleController.formatString("SlowmodeInfoSelected", R.string.SlowmodeInfoSelected, formatSeconds(seconds))); } } break; @@ -2686,13 +2745,15 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente actionCell.setText(LocaleController.getString("ChannelBlockUser", R.string.ChannelBlockUser), null, R.drawable.actions_removed, false); } else if (type == TYPE_ADMIN) { actionCell.setColors(Theme.key_windowBackgroundWhiteBlueIcon, Theme.key_windowBackgroundWhiteBlueButton); - actionCell.setText(LocaleController.getString("ChannelAddAdmin", R.string.ChannelAddAdmin), null, R.drawable.baseline_stars_24, true); + boolean showDivider = !(loadingUsers && !firstLoaded); + actionCell.setText(LocaleController.getString("ChannelAddAdmin", R.string.ChannelAddAdmin), null, R.drawable.baseline_stars_24, showDivider); } else if (type == TYPE_USERS) { actionCell.setColors(Theme.key_windowBackgroundWhiteBlueIcon, Theme.key_windowBackgroundWhiteBlueButton); + boolean showDivider = !(loadingUsers && !firstLoaded) && membersHeaderRow == -1 && !participants.isEmpty(); if (isChannel) { - actionCell.setText(LocaleController.getString("AddSubscriber", R.string.AddSubscriber), null, R.drawable.baseline_person_add_24, membersHeaderRow == -1 && !participants.isEmpty()); + actionCell.setText(LocaleController.getString("AddSubscriber", R.string.AddSubscriber), null, R.drawable.baseline_person_add_24, showDivider); } else { - actionCell.setText(LocaleController.getString("AddMember", R.string.AddMember), null, R.drawable.baseline_person_add_24, membersHeaderRow == -1 && !participants.isEmpty()); + actionCell.setText(LocaleController.getString("AddMember", R.string.AddMember), null, R.drawable.baseline_person_add_24, showDivider); } } } else if (position == recentActionsRow) { @@ -2825,6 +2886,8 @@ public class ChatUsersActivity extends BaseFragment implements NotificationCente return 8; } else if (position == slowmodeSelectRow) { return 9; + } else if (position == loadingProgressRow) { + return 10; } return 0; } 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 384f6e000..1d4a6c2ae 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java @@ -32,6 +32,7 @@ import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.inputmethod.EditorInfo; +import android.widget.Button; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; @@ -873,7 +874,7 @@ public class AlertsCreator { span = new URLSpanNoUnderline(span.getURL()) { @Override public void onClick(View widget) { - fragment.dismissCurrentDialig(); + fragment.dismissCurrentDialog(); super.onClick(widget); } }; @@ -1441,10 +1442,20 @@ public class AlertsCreator { final NumberPicker dayPicker = new NumberPicker(context); dayPicker.setTextOffset(AndroidUtilities.dp(10)); dayPicker.setItemCount(5); - final NumberPicker hourPicker = new NumberPicker(context); + final NumberPicker hourPicker = new NumberPicker(context) { + @Override + protected CharSequence getContentDescription(int value) { + return LocaleController.formatPluralString("Hours", value); + } + }; hourPicker.setItemCount(5); hourPicker.setTextOffset(-AndroidUtilities.dp(10)); - final NumberPicker minutePicker = new NumberPicker(context); + final NumberPicker minutePicker = new NumberPicker(context) { + @Override + protected CharSequence getContentDescription(int value) { + return LocaleController.formatPluralString("Minutes", value); + } + }; minutePicker.setItemCount(5); minutePicker.setTextOffset(-AndroidUtilities.dp(34)); @@ -1532,7 +1543,12 @@ public class AlertsCreator { calendar.setTimeInMillis(currentTime); int currentYear = calendar.get(Calendar.YEAR); - TextView buttonTextView = new TextView(context); + TextView buttonTextView = new TextView(context) { + @Override + public CharSequence getAccessibilityClassName() { + return Button.class.getName(); + } + }; linearLayout.addView(dayPicker, LayoutHelper.createLinear(0, 54 * 5, 0.5f)); dayPicker.setMinValue(0); @@ -1572,6 +1588,8 @@ public class AlertsCreator { currentDate *= 1000; calendar.setTimeInMillis(System.currentTimeMillis()); calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); calendar.set(Calendar.HOUR_OF_DAY, 0); int days = (int) ((currentDate - calendar.getTimeInMillis()) / (24 * 60 * 60 * 1000)); calendar.setTimeInMillis(currentDate); @@ -1614,7 +1632,7 @@ public class AlertsCreator { return builder; } - public static Dialog createMuteAlert(Context context, final long dialog_id) { + public static BottomSheet createMuteAlert(Context context, final long dialog_id) { if (context == null) { return null; } @@ -2562,6 +2580,22 @@ public class AlertsCreator { dialogId = -chat.id; } + int currentDate = ConnectionsManager.getInstance(currentAccount).getCurrentTime(); + boolean hasNonDiceMessages = false; + if (selectedMessage != null) { + hasNonDiceMessages = !selectedMessage.isDice() || Math.abs(currentDate - selectedMessage.messageOwner.date) > 24 * 60 * 60; + } else { + for (int a = 0; a < 2; a++) { + for (int b = 0; b < selectedMessages[a].size(); b++) { + MessageObject msg = selectedMessages[a].valueAt(b); + if (!msg.isDice() || Math.abs(currentDate - msg.messageOwner.date) > 24 * 60 * 60) { + hasNonDiceMessages = true; + break; + } + } + } + } + final boolean[] checks = new boolean[3]; final boolean[] deleteForAll = {true}; TLRPC.User actionUser = null; @@ -2578,7 +2612,6 @@ public class AlertsCreator { boolean canDeleteInbox = encryptedChat == null && user != null && canRevokeInbox && revokeTimeLimit == 0x7fffffff; if (chat != null && chat.megagroup && !scheduled) { boolean canBan = ChatObject.canBlockUsers(chat); - int currentDate = ConnectionsManager.getInstance(currentAccount).getCurrentTime(); if (selectedMessage != null) { if (selectedMessage.messageOwner.action == null || selectedMessage.messageOwner.action instanceof TLRPC.TL_messageActionEmpty || selectedMessage.messageOwner.action instanceof TLRPC.TL_messageActionChatDeleteUser || @@ -2686,7 +2719,7 @@ public class AlertsCreator { num++; } builder.setView(frameLayout); - } else if (!hasNotOut && myMessagesCount > 0) { + } else if (!hasNotOut && myMessagesCount > 0 && hasNonDiceMessages) { hasDeleteForAllCheck = true; FrameLayout frameLayout = new FrameLayout(activity); CheckBoxCell cell = new CheckBoxCell(activity, 1); @@ -2709,7 +2742,6 @@ public class AlertsCreator { actionUser = null; } } else if (!scheduled && !ChatObject.isChannel(chat) && encryptedChat == null) { - int currentDate = ConnectionsManager.getInstance(currentAccount).getCurrentTime(); if (user != null && user.id != UserConfig.getInstance(currentAccount).getClientUserId() && !user.bot || chat != null) { if (selectedMessage != null) { boolean hasOutgoing = !selectedMessage.isSendError() && (selectedMessage.messageOwner.action == null || selectedMessage.messageOwner.action instanceof TLRPC.TL_messageActionEmpty || selectedMessage.messageOwner.action instanceof TLRPC.TL_messageActionPhoneCall) && (selectedMessage.isOut() || canRevokeInbox || ChatObject.hasAdminRights(chat)) && (currentDate - selectedMessage.messageOwner.date) <= revokeTimeLimit; @@ -2736,7 +2768,7 @@ public class AlertsCreator { } } } - if (myMessagesCount > 0) { + if (myMessagesCount > 0 && hasNonDiceMessages) { hasDeleteForAllCheck = true; FrameLayout frameLayout = new FrameLayout(activity); CheckBoxCell cell = new CheckBoxCell(activity, 1); 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 c23a26164..d8fcd6487 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java @@ -30,9 +30,11 @@ import org.telegram.messenger.AnimatedFileDrawableStream; import org.telegram.messenger.DispatchQueue; import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; +import org.telegram.messenger.ImageLocation; import org.telegram.tgnet.TLRPC; import java.io.File; +import java.util.ArrayList; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -44,6 +46,7 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private static native void stopDecoder(long ptr); private static native int getVideoFrame(long ptr, Bitmap bitmap, int[] params, int stride, boolean preview, float startTimeSeconds, float endTimeSeconds); private static native void seekToMs(long ptr, long ms, boolean precise); + private static native int getFrameAtTime(long ptr, long ms, Bitmap bitmap, int[] data, int stride); private static native void prepareToSeek(long ptr); private static native void getVideoInfo(int sdkVersion, String src, int[] params); @@ -75,6 +78,7 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private boolean decoderCreated; private boolean decodeSingleFrame; private boolean singleFrameDecoded; + private boolean forceDecodeAfterNextFrame; private File path; private long streamFileSize; private int currentAccount; @@ -85,6 +89,8 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private int pendingRemoveLoadingFramesReset; private final Object sync = new Object(); + private boolean invalidateParentViewWithSecond; + private long lastFrameDecodeTime; private RectF actualDrawRect = new RectF(); @@ -113,7 +119,7 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private View parentView; - private View secondParentView; + private ArrayList secondParentViews = new ArrayList<>(); private AnimatedFileDrawableStream stream; @@ -123,9 +129,12 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, new ThreadPoolExecutor.DiscardPolicy()); protected final Runnable mInvalidateTask = () -> { - if (secondParentView != null) { - secondParentView.invalidate(); - } else if (parentView != null) { + if (!secondParentViews.isEmpty()) { + for (int a = 0, N = secondParentViews.size(); a < N; a++) { + secondParentViews.get(a).invalidate(); + } + } + if ((secondParentViews.isEmpty() || invalidateParentViewWithSecond) && parentView != null) { parentView.invalidate(); } }; @@ -187,7 +196,11 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { } else { pendingRemoveLoadingFramesReset--; } - singleFrameDecoded = true; + if (!forceDecodeAfterNextFrame) { + singleFrameDecoded = true; + } else { + forceDecodeAfterNextFrame = false; + } loadFrameTask = null; nextRenderingBitmap = backgroundBitmap; nextRenderingBitmapTime = backgroundBitmapTime; @@ -203,9 +216,12 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { invalidateAfter = 0; } lastTimeStamp = metaData[3]; - if (secondParentView != null) { - secondParentView.invalidate(); - } else if (parentView != null) { + if (!secondParentViews.isEmpty()) { + for (int a = 0, N = secondParentViews.size(); a < N; a++) { + secondParentViews.get(a).invalidate(); + } + } + if ((secondParentViews.isEmpty() || invalidateParentViewWithSecond) && parentView != null) { parentView.invalidate(); } scheduleNextGetFrame(); @@ -273,20 +289,23 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { }; private final Runnable mStartTask = () -> { - if (secondParentView != null) { - secondParentView.invalidate(); - } else if (parentView != null) { + if (!secondParentViews.isEmpty()) { + for (int a = 0, N = secondParentViews.size(); a < N; a++) { + secondParentViews.get(a).invalidate(); + } + } + if ((secondParentViews.isEmpty() || invalidateParentViewWithSecond) && parentView != null) { parentView.invalidate(); } }; - public AnimatedFileDrawable(File file, boolean createDecoder, long streamSize, TLRPC.Document document, Object parentObject, int account, boolean preview) { + public AnimatedFileDrawable(File file, boolean createDecoder, long streamSize, TLRPC.Document document, ImageLocation location, Object parentObject, long seekTo, int account, boolean preview) { path = file; streamFileSize = streamSize; currentAccount = account; - getPaint().setFlags(Paint.FILTER_BITMAP_FLAG); - if (streamSize != 0 && document != null) { - stream = new AnimatedFileDrawableStream(document, parentObject, account, preview); + getPaint().setFlags(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + if (streamSize != 0 && (document != null || location != null)) { + stream = new AnimatedFileDrawableStream(document, location, parentObject, account, preview); } if (createDecoder) { nativePtr = createDecoder(file.getAbsolutePath(), metaData, currentAccount, streamFileSize, stream, preview); @@ -296,9 +315,16 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { } decoderCreated = true; } + if (seekTo != 0) { + seekTo(seekTo, false); + } } public Bitmap getFrameAtTime(long ms) { + return getFrameAtTime(ms, false); + } + + public Bitmap getFrameAtTime(long ms, boolean precise) { if (!decoderCreated || nativePtr == 0) { return null; } @@ -306,11 +332,18 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { stream.cancel(false); stream.reset(); } - seekToMs(nativePtr, ms, false); + if (!precise) { + seekToMs(nativePtr, ms, precise); + } if (backgroundBitmap == null) { backgroundBitmap = Bitmap.createBitmap(metaData[0], metaData[1], Bitmap.Config.ARGB_8888); } - int result = getVideoFrame(nativePtr, backgroundBitmap, metaData, backgroundBitmap.getRowBytes(), true, 0, 0); + int result; + if (precise) { + result = getFrameAtTime(nativePtr, ms, backgroundBitmap, metaData, backgroundBitmap.getRowBytes()); + } else { + result = getVideoFrame(nativePtr, backgroundBitmap, metaData, backgroundBitmap.getRowBytes(), true, 0, 0); + } return result != 0 ? backgroundBitmap : null; } @@ -321,14 +354,27 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { parentView = view; } - public void setSecondParentView(View view) { - boolean hadSecond = secondParentView != null; - secondParentView = view; - if (hadSecond && view == null && roundRadiusBackup != null) { - setRoundRadius(roundRadiusBackup); + public void setInvalidateParentViewWithSecond(boolean value) { + invalidateParentViewWithSecond = value; + } + + public void addSecondParentView(View view) { + if (view == null || secondParentViews.contains(view)) { + return; } - if (view == null && recycleWithSecond) { - recycle(); + secondParentViews.add(view); + } + + public void removeSecondParentView(View view) { + secondParentViews.remove(view); + if (secondParentViews.isEmpty()) { + if (recycleWithSecond) { + recycle(); + } else { + if (roundRadiusBackup != null) { + setRoundRadius(roundRadiusBackup); + } + } } } @@ -340,20 +386,34 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { } public void seekTo(long ms, boolean removeLoading) { + seekTo(ms, removeLoading, false); + } + + public void seekTo(long ms, boolean removeLoading, boolean force) { synchronized (sync) { pendingSeekTo = ms; pendingSeekToUI = ms; - prepareToSeek(nativePtr); + if (nativePtr != 0) { + prepareToSeek(nativePtr); + } if (decoderCreated && stream != null) { stream.cancel(removeLoading); pendingRemoveLoading = removeLoading; pendingRemoveLoadingFramesReset = pendingRemoveLoading ? 0 : 10; } + if (force && decodeSingleFrame) { + singleFrameDecoded = false; + if (loadFrameTask == null) { + scheduleNextGetFrame(); + } else { + forceDecodeAfterNextFrame = true; + } + } } } public void recycle() { - if (secondParentView != null) { + if (!secondParentViews.isEmpty()) { recycleWithSecond = true; return; } @@ -658,19 +718,18 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { } public void setRoundRadius(int[] value) { - if (secondParentView != null) { + if (!secondParentViews.isEmpty()) { if (roundRadiusBackup == null) { roundRadiusBackup = new int[4]; } System.arraycopy(roundRadius, 0, roundRadiusBackup, 0, roundRadiusBackup.length); } - boolean changed = false; for (int i = 0; i < 4; i++) { - changed = value[i] != roundRadius[i]; + if (!invalidatePath && value[i] != roundRadius[i]) { + invalidatePath = true; + } roundRadius[i] = value[i]; } - getPaint().setFlags(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); - invalidatePath = changed; } private boolean hasRoundRadius() { @@ -693,9 +752,9 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { public AnimatedFileDrawable makeCopy() { AnimatedFileDrawable drawable; if (stream != null) { - drawable = new AnimatedFileDrawable(path, false, streamFileSize, stream.getDocument(), stream.getParentObject(), currentAccount, stream != null && stream.isPreview()); + drawable = new AnimatedFileDrawable(path, false, streamFileSize, stream.getDocument(), stream.getLocation(), stream.getParentObject(), pendingSeekToUI, currentAccount, stream != null && stream.isPreview()); } else { - drawable = new AnimatedFileDrawable(path, false, streamFileSize, null, null, currentAccount, stream != null && stream.isPreview()); + drawable = new AnimatedFileDrawable(path, false, streamFileSize, null, null, null, pendingSeekToUI, currentAccount, stream != null && stream.isPreview()); } drawable.metaData[0] = metaData[0]; drawable.metaData[1] = metaData[1]; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AudioPlayerAlert.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AudioPlayerAlert.java index 165ea9b62..0789f624b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AudioPlayerAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AudioPlayerAlert.java @@ -8,36 +8,40 @@ package org.telegram.ui.Components; +import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Paint; +import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import androidx.annotation.Keep; import androidx.core.content.FileProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import android.os.SystemClock; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.animation.DecelerateInterpolator; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; @@ -48,6 +52,7 @@ import org.telegram.messenger.DownloadController; import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.ImageLoader; +import org.telegram.messenger.ImageLocation; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; import org.telegram.messenger.MessageObject; @@ -63,10 +68,13 @@ import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.ActionBar.ActionBarMenuSubItem; import org.telegram.ui.ActionBar.AlertDialog; +import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.SimpleTextView; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.ActionBar.ThemeDescription; import org.telegram.ui.Cells.AudioPlayerCell; import org.telegram.ui.ChatActivity; import org.telegram.ui.DialogsActivity; @@ -74,27 +82,28 @@ import org.telegram.ui.LaunchActivity; import java.io.File; import java.util.ArrayList; -import java.util.Timer; -import java.util.TimerTask; public class AudioPlayerAlert extends BottomSheet implements NotificationCenter.NotificationCenterDelegate, DownloadController.FileDownloadProgressListener { private ActionBar actionBar; - private View shadow; - private View shadow2; - private ChatAvatarContainer avatarContainer; - private ActionBarMenuItem searchItem; - private ActionBarMenuItem menuItem; + private View actionBarShadow; + private View playerShadow; private boolean searchWas; private boolean searching; private RecyclerListView listView; private LinearLayoutManager layoutManager; private ListAdapter listAdapter; + private LinearLayout emptyView; + private ImageView emptyImageView; + private TextView emptyTitleTextView; + private TextView emptySubtitleTextView; private FrameLayout playerLayout; private BackupImageView placeholderImageView; private TextView titleTextView; + private ImageView prevButton; + private ImageView nextButton; private TextView authorTextView; private ActionBarMenuItem optionsButton; private LineProgressView progressView; @@ -102,47 +111,42 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. private SimpleTextView timeTextView; private ImageView playbackSpeedButton; private TextView durationTextView; - private ActionBarMenuItem shuffleButton; + private ActionBarMenuItem repeatButton; + private ActionBarMenuSubItem repeatSongItem; + private ActionBarMenuSubItem repeatListItem; + private ActionBarMenuSubItem shuffleListItem; + private ActionBarMenuSubItem reverseOrderItem; private ImageView playButton; - private ImageView repeatButton; + private FrameLayout blurredView; + private BackupImageView bigAlbumConver; + private ActionBarMenuItem searchItem; + private boolean blurredAnimationInProgress; private View[] buttons = new View[5]; - private Drawable[] playOrderButtons = new Drawable[2]; - private boolean hasOptions = true; private boolean draggingSeekBar; - private boolean scrollToSong = true; + private long lastBufferedPositionCheck; + private boolean currentAudioFinishedLoading; - private boolean isInFullMode; - private AnimatorSet animatorSet; - private float fullAnimationProgress; - private float startTranslation; - private float endTranslation; - private float panelStartTranslation; - private float panelEndTranslation; + private boolean scrollToSong = true; private int searchOpenPosition = -1; private int searchOpenOffset; - private int hasNoCover; - private Drawable noCoverDrawable; - private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - - private float thumbMaxScale; - private int thumbMaxX; - private int thumbMaxY; - private ArrayList playlist; private int scrollOffsetY = Integer.MAX_VALUE; private int topBeforeSwitch; - private Drawable shadowDrawable; private boolean inFullSize; + private String currentFile; + private AnimatorSet actionBarAnimation; private int lastTime; + private int lastDuration; + private int TAG; private LaunchActivity parentActivity; @@ -158,32 +162,23 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. } parentActivity = (LaunchActivity) context; - noCoverDrawable = context.getResources().getDrawable(R.drawable.nocover).mutate(); - noCoverDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_placeholder), PorterDuff.Mode.SRC_IN)); TAG = DownloadController.getInstance(currentAccount).generateObserverTag(); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.messagePlayingDidReset); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.messagePlayingPlayStateChanged); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.messagePlayingDidStart); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.fileDidLoad); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.FileLoadProgressChanged); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.musicDidLoad); - - shadowDrawable = context.getResources().getDrawable(R.drawable.sheet_shadow).mutate(); - shadowDrawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_background), PorterDuff.Mode.SRC_IN)); - paint.setColor(Theme.getColor(Theme.key_player_placeholderBackground)); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.moreMusicDidLoad); containerView = new FrameLayout(context) { + private RectF rect = new RectF(); private boolean ignoreLayout = false; - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN && scrollOffsetY != 0 && ev.getY() < scrollOffsetY && placeholderImageView.getTranslationX() == 0) { - dismiss(); - return true; - } - return super.onInterceptTouchEvent(ev); - } + private int lastMeasturedHeight; + private int lastMeasturedWidth; @Override public boolean onTouchEvent(MotionEvent e) { @@ -192,49 +187,78 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int height = MeasureSpec.getSize(heightMeasureSpec); - int contentSize = AndroidUtilities.dp(178) + playlist.size() * AndroidUtilities.dp(56) + backgroundPaddingTop + ActionBar.getCurrentActionBarHeight() + AndroidUtilities.statusBarHeight; - int padding; - heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); - if (searching) { - padding = AndroidUtilities.dp(178) + ActionBar.getCurrentActionBarHeight() + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); - } else { - if (contentSize < height) { - padding = height - contentSize; - } else { - padding = (contentSize < height ? 0 : height - (height / 5 * 3)); + int totalHeight = MeasureSpec.getSize(heightMeasureSpec); + int w = MeasureSpec.getSize(widthMeasureSpec); + if (totalHeight != lastMeasturedHeight || w != lastMeasturedWidth) { + if (blurredView.getTag() != null) { + showAlbumCover(false, false); + } + lastMeasturedWidth = w; + lastMeasturedHeight = totalHeight; + } + ignoreLayout = true; + if (Build.VERSION.SDK_INT >= 21 && !isFullscreen) { + setPadding(backgroundPaddingLeft, AndroidUtilities.statusBarHeight, backgroundPaddingLeft, 0); + } + playerLayout.setVisibility(searchWas || keyboardVisible ? INVISIBLE : VISIBLE); + playerShadow.setVisibility(playerLayout.getVisibility()); + int availableHeight = totalHeight - getPaddingTop(); + + LayoutParams layoutParams = (LayoutParams) listView.getLayoutParams(); + layoutParams.topMargin = ActionBar.getCurrentActionBarHeight(); + + layoutParams = (LayoutParams) actionBarShadow.getLayoutParams(); + layoutParams.topMargin = ActionBar.getCurrentActionBarHeight(); + + layoutParams = (LayoutParams) blurredView.getLayoutParams(); + layoutParams.topMargin = -getPaddingTop(); + + int contentSize = AndroidUtilities.dp(179); + if (playlist.size() > 1) { + contentSize += backgroundPaddingTop + playlist.size() * AndroidUtilities.dp(56); + } + int padding; + if (searching || keyboardVisible) { + padding = AndroidUtilities.dp(8); + } else { + padding = (contentSize < availableHeight ? availableHeight - contentSize : availableHeight - (int) (availableHeight / 5 * 3.5f)) + AndroidUtilities.dp(8); + if (padding > availableHeight - AndroidUtilities.dp(179 + 150)) { + padding = availableHeight - AndroidUtilities.dp(179 + 150); + } + if (padding < 0) { + padding = 0; } - padding += ActionBar.getCurrentActionBarHeight() + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); } if (listView.getPaddingTop() != padding) { - ignoreLayout = true; - listView.setPadding(0, padding, 0, AndroidUtilities.dp(8)); - ignoreLayout = false; - } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - inFullSize = getMeasuredHeight() >= height; - int availableHeight = height - ActionBar.getCurrentActionBarHeight() - (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0) - AndroidUtilities.dp(120); - int maxSize = Math.max(availableHeight, getMeasuredWidth()); - thumbMaxX = (getMeasuredWidth() - maxSize) / 2 - AndroidUtilities.dp(17); - thumbMaxY = AndroidUtilities.dp(19); - panelEndTranslation = getMeasuredHeight() - playerLayout.getMeasuredHeight(); - thumbMaxScale = maxSize / (float) placeholderImageView.getMeasuredWidth() - 1.0f; - - endTranslation = ActionBar.getCurrentActionBarHeight() + (AndroidUtilities.statusBarHeight - AndroidUtilities.dp(19)); - int scaledHeight = (int) Math.ceil(placeholderImageView.getMeasuredHeight() * (1.0f + thumbMaxScale)); - if (scaledHeight > availableHeight) { - endTranslation -= (scaledHeight - availableHeight); + listView.setPadding(0, padding, 0, searching && keyboardVisible ? 0 : listView.getPaddingBottom()); } + ignoreLayout = false; + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(totalHeight, MeasureSpec.EXACTLY)); + inFullSize = getMeasuredHeight() >= totalHeight; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - int y = actionBar.getMeasuredHeight(); - shadow.layout(shadow.getLeft(), y, shadow.getRight(), y + shadow.getMeasuredHeight()); updateLayout(); + updateEmptyViewPosition(); + } - setFullAnimationProgress(fullAnimationProgress); + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN && scrollOffsetY != 0 && actionBar.getAlpha() == 0.0f) { + boolean dismiss; + if (listAdapter.getItemCount() > 0) { + dismiss = ev.getY() < scrollOffsetY + AndroidUtilities.dp(12); + } else { + dismiss = ev.getY() < getMeasuredHeight() - AndroidUtilities.dp(179 + 12); + } + if (dismiss) { + dismiss(); + return true; + } + } + return super.onInterceptTouchEvent(ev); } @Override @@ -247,28 +271,84 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. @Override protected void onDraw(Canvas canvas) { - shadowDrawable.setBounds(0, Math.max(actionBar.getMeasuredHeight(), scrollOffsetY) - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); - shadowDrawable.draw(canvas); + if (playlist.size() <= 1) { + shadowDrawable.setBounds(0, getMeasuredHeight() - playerLayout.getMeasuredHeight() - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); + shadowDrawable.draw(canvas); + } else { + int offset = AndroidUtilities.dp(13); + int top = scrollOffsetY - backgroundPaddingTop - offset; + if (currentSheetAnimationType == 1) { + top += listView.getTranslationY(); + } + int y = top + AndroidUtilities.dp(20); + + int height = getMeasuredHeight() + AndroidUtilities.dp(15) + backgroundPaddingTop; + float rad = 1.0f; + + if (top + backgroundPaddingTop < ActionBar.getCurrentActionBarHeight()) { + float toMove = offset + AndroidUtilities.dp(11 - 7); + float moveProgress = Math.min(1.0f, (ActionBar.getCurrentActionBarHeight() - top - backgroundPaddingTop) / toMove); + float availableToMove = ActionBar.getCurrentActionBarHeight() - toMove; + + int diff = (int) (availableToMove * moveProgress); + top -= diff; + y -= diff; + height += diff; + rad = 1.0f - moveProgress; + } + + if (Build.VERSION.SDK_INT >= 21) { + top += AndroidUtilities.statusBarHeight; + y += AndroidUtilities.statusBarHeight; + } + + shadowDrawable.setBounds(0, top, getMeasuredWidth(), height); + shadowDrawable.draw(canvas); + + if (rad != 1.0f) { + Theme.dialogs_onlineCirclePaint.setColor(Theme.getColor(Theme.key_dialogBackground)); + rect.set(backgroundPaddingLeft, backgroundPaddingTop + top, getMeasuredWidth() - backgroundPaddingLeft, backgroundPaddingTop + top + AndroidUtilities.dp(24)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(12) * rad, AndroidUtilities.dp(12) * rad, Theme.dialogs_onlineCirclePaint); + } + + if (rad != 0) { + float alphaProgress = 1.0f; + int w = AndroidUtilities.dp(36); + rect.set((getMeasuredWidth() - w) / 2, y, (getMeasuredWidth() + w) / 2, y + AndroidUtilities.dp(4)); + int color = Theme.getColor(Theme.key_sheet_scrollUp); + int alpha = Color.alpha(color); + Theme.dialogs_onlineCirclePaint.setColor(color); + Theme.dialogs_onlineCirclePaint.setAlpha((int) (alpha * alphaProgress * rad)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(2), AndroidUtilities.dp(2), Theme.dialogs_onlineCirclePaint); + } + + int color1 = Theme.getColor(Theme.key_dialogBackground); + int finalColor = Color.argb((int) (255 * actionBar.getAlpha()), (int) (Color.red(color1) * 0.8f), (int) (Color.green(color1) * 0.8f), (int) (Color.blue(color1) * 0.8f)); + Theme.dialogs_onlineCirclePaint.setColor(finalColor); + canvas.drawRect(backgroundPaddingLeft, 0, getMeasuredWidth() - backgroundPaddingLeft, AndroidUtilities.statusBarHeight, Theme.dialogs_onlineCirclePaint); + } } }; containerView.setWillNotDraw(false); containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); - actionBar = new ActionBar(context); + actionBar = new ActionBar(context) { + @Override + public void setAlpha(float alpha) { + super.setAlpha(alpha); + containerView.invalidate(); + } + }; actionBar.setBackgroundColor(Theme.getColor(Theme.key_player_actionBar)); actionBar.setBackButtonImage(R.drawable.ic_ab_back); - actionBar.setItemsColor(Theme.getColor(Theme.key_player_actionBarItems), false); + actionBar.setItemsColor(Theme.getColor(Theme.key_player_actionBarTitle), false); actionBar.setItemsBackgroundColor(Theme.getColor(Theme.key_player_actionBarSelector), false); actionBar.setTitleColor(Theme.getColor(Theme.key_player_actionBarTitle)); + actionBar.setTitle(LocaleController.getString("AttachMusic", R.string.AttachMusic)); actionBar.setSubtitleColor(Theme.getColor(Theme.key_player_actionBarSubtitle)); + actionBar.setOccupyStatusBar(false); actionBar.setAlpha(0.0f); - actionBar.setTitle("1"); - actionBar.setSubtitle("1"); - actionBar.getTitleTextView().setAlpha(0.0f); - actionBar.getSubtitleTextView().setAlpha(0.0f); - avatarContainer = new ChatAvatarContainer(context, null, false); - avatarContainer.setEnabled(false); - avatarContainer.setTitleColors(Theme.getColor(Theme.key_player_actionBarTitle), Theme.getColor(Theme.key_player_actionBarSubtitle)); + if (messageObject != null) { long did = messageObject.getDialogId(); int lower_id = (int) did; @@ -277,14 +357,12 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. if (lower_id > 0) { TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(lower_id); if (user != null) { - avatarContainer.setTitle(ContactsController.formatName(user.first_name, user.last_name)); - avatarContainer.setUserAvatar(user); + actionBar.setTitle(ContactsController.formatName(user.first_name, user.last_name)); } } else { TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(-lower_id); if (chat != null) { - avatarContainer.setTitle(chat.title); - avatarContainer.setChatAvatar(chat); + actionBar.setTitle(chat.title); } } } else { @@ -292,31 +370,16 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. if (encryptedChat != null) { TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(encryptedChat.user_id); if (user != null) { - avatarContainer.setTitle(ContactsController.formatName(user.first_name, user.last_name)); - avatarContainer.setUserAvatar(user); + actionBar.setTitle(ContactsController.formatName(user.first_name, user.last_name)); } } } } - avatarContainer.setSubtitle(LocaleController.getString("AudioTitle", R.string.AudioTitle)); - actionBar.addView(avatarContainer, 0, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 56, 0, 40, 0)); ActionBarMenu menu = actionBar.createMenu(); - menuItem = menu.addItem(0, R.drawable.ic_ab_other); - menuItem.addSubItem(1, R.drawable.msg_forward, LocaleController.getString("Forward", R.string.Forward)); - menuItem.addSubItem(2, R.drawable.baseline_share_24, LocaleController.getString("ShareFile", R.string.ShareFile)); - menuItem.addSubItem(4, R.drawable.msg_message, LocaleController.getString("ShowInChat", R.string.ShowInChat)); - menuItem.setContentDescription(LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions)); - menuItem.setTranslationX(AndroidUtilities.dp(48)); - menuItem.setAlpha(0.0f); - searchItem = menu.addItem(0, R.drawable.ic_ab_search).setIsSearchField(true).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { @Override public void onSearchCollapse() { - avatarContainer.setVisibility(View.VISIBLE); - if (hasOptions) { - menuItem.setVisibility(View.INVISIBLE); - } if (searching) { searchWas = false; searching = false; @@ -329,12 +392,7 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. public void onSearchExpand() { searchOpenPosition = layoutManager.findLastVisibleItemPosition(); View firstVisView = layoutManager.findViewByPosition(searchOpenPosition); - searchOpenOffset = ((firstVisView == null) ? 0 : firstVisView.getTop()) - listView.getPaddingTop(); - - avatarContainer.setVisibility(View.GONE); - if (hasOptions) { - menuItem.setVisibility(View.GONE); - } + searchOpenOffset = firstVisView == null ? 0 : firstVisView.getTop(); searching = true; setAllowNestedScroll(false); listAdapter.notifyDataSetChanged(); @@ -357,10 +415,6 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. editText.setHintTextColor(Theme.getColor(Theme.key_player_time)); editText.setCursorColor(Theme.getColor(Theme.key_player_actionBarTitle)); - if (!AndroidUtilities.isTablet()) { - actionBar.showActionModeTop(); - actionBar.setActionModeTopColor(Theme.getColor(Theme.key_player_actionBarTop)); - } actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { @Override public void onItemClick(int id) { @@ -372,13 +426,12 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. } }); - shadow = new View(context); - shadow.setAlpha(0.0f); - shadow.setBackgroundResource(R.drawable.header_shadow); + actionBarShadow = new View(context); + actionBarShadow.setAlpha(0.0f); + actionBarShadow.setBackgroundResource(R.drawable.header_shadow); - shadow2 = new View(context); - shadow2.setAlpha(0.0f); - shadow2.setBackgroundResource(R.drawable.header_shadow); + playerShadow = new View(context); + playerShadow.setBackgroundColor(Theme.getColor(Theme.key_dialogShadowLine)); playerLayout = new FrameLayout(context) { @Override @@ -390,109 +443,49 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. } } }; - playerLayout.setBackgroundColor(Theme.getColor(Theme.key_player_background)); placeholderImageView = new BackupImageView(context) { - private RectF rect = new RectF(); + private long pressTime; @Override - protected void onDraw(Canvas canvas) { - if (hasNoCover == 1 || hasNoCover == 2 && (!getImageReceiver().hasBitmapImage() || getImageReceiver().getCurrentAlpha() != 1.0f)) { - rect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); - canvas.drawRoundRect(rect, getRoundRadius()[0], getRoundRadius()[0], paint); - float plusScale = thumbMaxScale / getScaleX() / 3; - int s = (int) (AndroidUtilities.dp(63) * Math.max(plusScale / thumbMaxScale, 1.0f / thumbMaxScale)); - int x = (int) (rect.centerX() - s / 2); - int y = (int) (rect.centerY() - s / 2); - noCoverDrawable.setBounds(x, y, x + s, y + s); - noCoverDrawable.draw(canvas); - } - if (hasNoCover != 1) { - super.onDraw(canvas); - } - } - }; - placeholderImageView.setRoundRadius(AndroidUtilities.dp(20)); - placeholderImageView.setPivotX(0); - placeholderImageView.setPivotY(0); - placeholderImageView.setOnClickListener(view -> { - if (animatorSet != null) { - animatorSet.cancel(); - animatorSet = null; - } - animatorSet = new AnimatorSet(); - if (scrollOffsetY <= actionBar.getMeasuredHeight()) { - animatorSet.playTogether(ObjectAnimator.ofFloat(AudioPlayerAlert.this, "fullAnimationProgress", isInFullMode ? 0.0f : 1.0f)); - } else { - animatorSet.playTogether(ObjectAnimator.ofFloat(AudioPlayerAlert.this, "fullAnimationProgress", isInFullMode ? 0.0f : 1.0f), - ObjectAnimator.ofFloat(actionBar, "alpha", isInFullMode ? 0.0f : 1.0f), - ObjectAnimator.ofFloat(shadow, "alpha", isInFullMode ? 0.0f : 1.0f), - ObjectAnimator.ofFloat(shadow2, "alpha", isInFullMode ? 0.0f : 1.0f)); - } - - animatorSet.setInterpolator(new DecelerateInterpolator()); - animatorSet.setDuration(250); - animatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (animation.equals(animatorSet)) { - if (!isInFullMode) { - listView.setScrollEnabled(true); - if (hasOptions) { - menuItem.setVisibility(View.INVISIBLE); - } - searchItem.setVisibility(View.VISIBLE); - } else { - if (hasOptions) { - menuItem.setVisibility(View.VISIBLE); - } - searchItem.setVisibility(View.INVISIBLE); - } - animatorSet = null; + public boolean onTouchEvent(MotionEvent event) { + int action = event.getAction(); + if (action == MotionEvent.ACTION_DOWN) { + if (imageReceiver.hasBitmapImage()) { + showAlbumCover(true, true); + pressTime = SystemClock.elapsedRealtime(); + } + } else if (action != MotionEvent.ACTION_MOVE) { + if (SystemClock.elapsedRealtime() - pressTime >= 400) { + showAlbumCover(false, true); } } - }); - animatorSet.start(); - if (hasOptions) { - menuItem.setVisibility(View.VISIBLE); + return true; } - searchItem.setVisibility(View.VISIBLE); - isInFullMode = !isInFullMode; - listView.setScrollEnabled(false); - if (isInFullMode) { - shuffleButton.setAdditionalYOffset(-AndroidUtilities.dp(20 + 48)); - } else { - shuffleButton.setAdditionalYOffset(-AndroidUtilities.dp(10)); + }; + placeholderImageView.getImageReceiver().setDelegate((imageReceiver, set, thumb, memCache) -> { + if (blurredView.getTag() != null) { + bigAlbumConver.setImageBitmap(placeholderImageView.imageReceiver.getBitmap()); } }); + placeholderImageView.setRoundRadius(AndroidUtilities.dp(4)); + playerLayout.addView(placeholderImageView, LayoutHelper.createFrame(44, 44, Gravity.TOP | Gravity.RIGHT, 0, 20, 20, 0)); titleTextView = new TextView(context); titleTextView.setTextColor(Theme.getColor(Theme.key_player_actionBarTitle)); - titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 17); titleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); titleTextView.setEllipsize(TextUtils.TruncateAt.END); titleTextView.setSingleLine(true); - playerLayout.addView(titleTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 72, 18, 60, 0)); + playerLayout.addView(titleTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 20, 20, 72, 0)); authorTextView = new TextView(context); authorTextView.setTextColor(Theme.getColor(Theme.key_player_time)); - authorTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + authorTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); authorTextView.setEllipsize(TextUtils.TruncateAt.END); authorTextView.setSingleLine(true); - playerLayout.addView(authorTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 72, 40, 60, 0)); - - optionsButton = new ActionBarMenuItem(context, null, 0, Theme.getColor(Theme.key_player_actionBarItems)); - optionsButton.setLongClickEnabled(false); - optionsButton.setIcon(R.drawable.ic_ab_other); - optionsButton.setAdditionalYOffset(-AndroidUtilities.dp(120)); - playerLayout.addView(optionsButton, LayoutHelper.createFrame(40, 40, Gravity.TOP | Gravity.RIGHT, 0, 19, 10, 0)); - optionsButton.addSubItem(1, R.drawable.msg_forward, LocaleController.getString("Forward", R.string.Forward)); - optionsButton.addSubItem(2, R.drawable.baseline_share_24, LocaleController.getString("ShareFile", R.string.ShareFile)); - optionsButton.addSubItem(4, R.drawable.msg_message, LocaleController.getString("ShowInChat", R.string.ShowInChat)); - optionsButton.setOnClickListener(v -> optionsButton.toggleSubMenu()); - optionsButton.setDelegate(this::onSubItemClick); - optionsButton.setContentDescription(LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions)); + playerLayout.addView(authorTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 20, 47, 72, 0)); seekBarView = new SeekBarView(context); seekBarView.setDelegate(new SeekBarView.SeekBarViewDelegate() { @@ -511,27 +504,36 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. public void onSeekBarPressed(boolean pressed) { draggingSeekBar = pressed; } + + @Override + public CharSequence getContentDescription() { + final String time = LocaleController.formatPluralString("Minutes", lastTime / 60) + ' ' + LocaleController.formatPluralString("Seconds", lastTime % 60); + final String totalTime = LocaleController.formatPluralString("Minutes", lastDuration / 60) + ' ' + LocaleController.formatPluralString("Seconds", lastDuration % 60); + return LocaleController.formatString("AccDescrPlayerDuration", R.string.AccDescrPlayerDuration, time, totalTime); + } }); seekBarView.setReportChanges(true); - playerLayout.addView(seekBarView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 38, Gravity.TOP | Gravity.LEFT, 4, 58, 4, 0)); + playerLayout.addView(seekBarView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 38, Gravity.TOP | Gravity.LEFT, 5, 70, 5, 0)); progressView = new LineProgressView(context); progressView.setVisibility(View.INVISIBLE); progressView.setBackgroundColor(Theme.getColor(Theme.key_player_progressBackground)); progressView.setProgressColor(Theme.getColor(Theme.key_player_progress)); - playerLayout.addView(progressView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 2, Gravity.TOP | Gravity.LEFT, 20, 78, 20, 0)); + playerLayout.addView(progressView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 2, Gravity.TOP | Gravity.LEFT, 21, 90, 21, 0)); timeTextView = new SimpleTextView(context); timeTextView.setTextSize(12); timeTextView.setText("0:00"); timeTextView.setTextColor(Theme.getColor(Theme.key_player_time)); - playerLayout.addView(timeTextView, LayoutHelper.createFrame(100, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 20, 92, 0, 0)); + timeTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + playerLayout.addView(timeTextView, LayoutHelper.createFrame(100, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 20, 98, 0, 0)); durationTextView = new TextView(context); durationTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); durationTextView.setTextColor(Theme.getColor(Theme.key_player_time)); durationTextView.setGravity(Gravity.CENTER); - playerLayout.addView(durationTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.RIGHT, 0, 90, 20, 0)); + durationTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + playerLayout.addView(durationTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.RIGHT, 0, 96, 20, 0)); playbackSpeedButton = new ImageView(context); playbackSpeedButton.setScaleType(ImageView.ScaleType.CENTER); @@ -540,7 +542,7 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. if (AndroidUtilities.density >= 3.0f) { playbackSpeedButton.setPadding(0, 1, 0, 0); } - playerLayout.addView(playbackSpeedButton, LayoutHelper.createFrame(36, 36, Gravity.TOP | Gravity.RIGHT, 0, 80, 20, 0)); + playerLayout.addView(playbackSpeedButton, LayoutHelper.createFrame(36, 36, Gravity.TOP | Gravity.RIGHT, 0, 86, 20, 0)); playbackSpeedButton.setOnClickListener(v -> { float currentPlaybackSpeed = MediaController.getInstance().getPlaybackSpeed(true); if (currentPlaybackSpeed > 1) { @@ -563,43 +565,75 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. } } }; - playerLayout.addView(bottomView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 66, Gravity.TOP | Gravity.LEFT, 0, 106, 0, 0)); + playerLayout.addView(bottomView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 66, Gravity.TOP | Gravity.LEFT, 0, 111, 0, 0)); - buttons[0] = shuffleButton = new ActionBarMenuItem(context, null, 0, 0); - shuffleButton.setLongClickEnabled(false); - shuffleButton.setAdditionalYOffset(-AndroidUtilities.dp(10)); - bottomView.addView(shuffleButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP)); - shuffleButton.setOnClickListener(v -> shuffleButton.toggleSubMenu()); + buttons[0] = repeatButton = new ActionBarMenuItem(context, null, 0, 0); + repeatButton.setLongClickEnabled(false); + repeatButton.setShowSubmenuByMove(false); + repeatButton.setAdditionalYOffset(-AndroidUtilities.dp(166)); + if (Build.VERSION.SDK_INT >= 21) { + repeatButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_listSelector), 1, AndroidUtilities.dp(18))); + } + bottomView.addView(repeatButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP)); + repeatButton.setOnClickListener(v -> { + updateSubMenu(); + repeatButton.toggleSubMenu(); + }); + repeatSongItem = repeatButton.addSubItem(3, R.drawable.player_new_repeatone, LocaleController.getString("RepeatSong", R.string.RepeatSong)); + repeatListItem = repeatButton.addSubItem(4, R.drawable.player_new_repeatall, LocaleController.getString("RepeatList", R.string.RepeatList)); + shuffleListItem = repeatButton.addSubItem(2, R.drawable.player_new_shuffle, LocaleController.getString("ShuffleList", R.string.ShuffleList)); + reverseOrderItem = repeatButton.addSubItem(1, R.drawable.player_new_order, LocaleController.getString("ReverseOrder", R.string.ReverseOrder)); + repeatButton.setShowedFromBottom(true); - TextView textView = shuffleButton.addSubItem(1, LocaleController.getString("ReverseOrder", R.string.ReverseOrder)); - textView.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(16), 0); - playOrderButtons[0] = context.getResources().getDrawable(R.drawable.music_reverse).mutate(); - textView.setCompoundDrawablePadding(AndroidUtilities.dp(8)); - textView.setCompoundDrawablesWithIntrinsicBounds(playOrderButtons[0], null, null, null); - - textView = shuffleButton.addSubItem(2, LocaleController.getString("Shuffle", R.string.Shuffle)); - textView.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(16), 0); - playOrderButtons[1] = context.getResources().getDrawable(R.drawable.pl_shuffle).mutate(); - textView.setCompoundDrawablePadding(AndroidUtilities.dp(8)); - textView.setCompoundDrawablesWithIntrinsicBounds(playOrderButtons[1], null, null, null); - - shuffleButton.setDelegate(id -> { - MediaController.getInstance().toggleShuffleMusic(id); - updateShuffleButton(); - listAdapter.notifyDataSetChanged(); + repeatButton.setDelegate(id -> { + if (id == 1 || id == 2) { + boolean oldReversed = SharedConfig.playOrderReversed; + if (SharedConfig.playOrderReversed && id == 1 || SharedConfig.shuffleMusic && id == 2) { + MediaController.getInstance().setPlaybackOrderType(0); + } else { + MediaController.getInstance().setPlaybackOrderType(id); + } + listAdapter.notifyDataSetChanged(); + if (oldReversed != SharedConfig.playOrderReversed) { + listView.stopScroll(); + scrollToCurrentSong(false); + } + } else { + if (id == 4) { + if (SharedConfig.repeatMode == 1) { + SharedConfig.setRepeatMode(0); + } else { + SharedConfig.setRepeatMode(1); + } + } else { + if (SharedConfig.repeatMode == 2) { + SharedConfig.setRepeatMode(0); + } else { + SharedConfig.setRepeatMode(2); + } + } + } + updateRepeatButton(); }); - ImageView prevButton; buttons[1] = prevButton = new ImageView(context); prevButton.setScaleType(ImageView.ScaleType.CENTER); - prevButton.setImageDrawable(Theme.createSimpleSelectorDrawable(context, R.drawable.pl_previous, Theme.getColor(Theme.key_player_button), Theme.getColor(Theme.key_player_buttonActive))); + prevButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_button), PorterDuff.Mode.MULTIPLY)); + prevButton.setImageResource(R.drawable.player_new_previous); + if (Build.VERSION.SDK_INT >= 21) { + prevButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_listSelector), 1, AndroidUtilities.dp(22))); + } bottomView.addView(prevButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP)); prevButton.setOnClickListener(v -> MediaController.getInstance().playPreviousMessage()); prevButton.setContentDescription(LocaleController.getString("AccDescrPrevious", R.string.AccDescrPrevious)); buttons[2] = playButton = new ImageView(context); playButton.setScaleType(ImageView.ScaleType.CENTER); - playButton.setImageDrawable(Theme.createSimpleSelectorDrawable(context, R.drawable.pl_play, Theme.getColor(Theme.key_player_button), Theme.getColor(Theme.key_player_buttonActive))); + playButton.setImageResource(R.drawable.player_new_play); + playButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_button), PorterDuff.Mode.MULTIPLY)); + if (Build.VERSION.SDK_INT >= 21) { + playButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_listSelector), 1, AndroidUtilities.dp(24))); + } bottomView.addView(playButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP)); playButton.setOnClickListener(v -> { if (MediaController.getInstance().isDownloadingCurrentMessage()) { @@ -612,22 +646,63 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. } }); - ImageView nextButton; buttons[3] = nextButton = new ImageView(context); nextButton.setScaleType(ImageView.ScaleType.CENTER); - nextButton.setImageDrawable(Theme.createSimpleSelectorDrawable(context, R.drawable.pl_next, Theme.getColor(Theme.key_player_button), Theme.getColor(Theme.key_player_buttonActive))); + nextButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_button), PorterDuff.Mode.MULTIPLY)); + nextButton.setImageResource(R.drawable.player_new_next); + if (Build.VERSION.SDK_INT >= 21) { + nextButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_listSelector), 1, AndroidUtilities.dp(22))); + } bottomView.addView(nextButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP)); nextButton.setOnClickListener(v -> MediaController.getInstance().playNextMessage()); nextButton.setContentDescription(LocaleController.getString("Next", R.string.Next)); - buttons[4] = repeatButton = new ImageView(context); - repeatButton.setScaleType(ImageView.ScaleType.CENTER); - repeatButton.setPadding(0, 0, AndroidUtilities.dp(8), 0); - bottomView.addView(repeatButton, LayoutHelper.createFrame(50, 48, Gravity.LEFT | Gravity.TOP)); - repeatButton.setOnClickListener(v -> { - SharedConfig.toggleRepeatMode(); - updateRepeatButton(); - }); + buttons[4] = optionsButton = new ActionBarMenuItem(context, null, 0, Theme.getColor(Theme.key_player_button)); + optionsButton.setLongClickEnabled(false); + optionsButton.setShowSubmenuByMove(false); + optionsButton.setIcon(R.drawable.ic_ab_other); + optionsButton.setSubMenuOpenSide(2); + optionsButton.setAdditionalYOffset(-AndroidUtilities.dp(157)); + if (Build.VERSION.SDK_INT >= 21) { + optionsButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_listSelector), 1, AndroidUtilities.dp(18))); + } + bottomView.addView(optionsButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP)); + optionsButton.addSubItem(1, R.drawable.msg_forward, LocaleController.getString("Forward", R.string.Forward)); + optionsButton.addSubItem(2, R.drawable.msg_shareout, LocaleController.getString("ShareFile", R.string.ShareFile)); + optionsButton.addSubItem(5, R.drawable.msg_download, LocaleController.getString("SaveToMusic", R.string.SaveToMusic)); + optionsButton.addSubItem(4, R.drawable.msg_message, LocaleController.getString("ShowInChat", R.string.ShowInChat)); + optionsButton.setShowedFromBottom(true); + optionsButton.setOnClickListener(v -> optionsButton.toggleSubMenu()); + optionsButton.setDelegate(this::onSubItemClick); + optionsButton.setContentDescription(LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions)); + + emptyView = new LinearLayout(context); + emptyView.setOrientation(LinearLayout.VERTICAL); + emptyView.setGravity(Gravity.CENTER); + emptyView.setVisibility(View.GONE); + containerView.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + emptyView.setOnTouchListener((v, event) -> true); + + emptyImageView = new ImageView(context); + emptyImageView.setImageResource(R.drawable.music_empty); + emptyImageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogEmptyImage), PorterDuff.Mode.MULTIPLY)); + emptyView.addView(emptyImageView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT)); + + emptyTitleTextView = new TextView(context); + emptyTitleTextView.setTextColor(Theme.getColor(Theme.key_dialogEmptyText)); + emptyTitleTextView.setGravity(Gravity.CENTER); + emptyTitleTextView.setText(LocaleController.getString("NoAudioFound", R.string.NoAudioFound)); + emptyTitleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + emptyTitleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 17); + emptyTitleTextView.setPadding(AndroidUtilities.dp(40), 0, AndroidUtilities.dp(40), 0); + emptyView.addView(emptyTitleTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER, 0, 11, 0, 0)); + + emptySubtitleTextView = new TextView(context); + emptySubtitleTextView.setTextColor(Theme.getColor(Theme.key_dialogEmptyText)); + emptySubtitleTextView.setGravity(Gravity.CENTER); + emptySubtitleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + emptySubtitleTextView.setPadding(AndroidUtilities.dp(40), 0, AndroidUtilities.dp(40), 0); + emptyView.addView(emptySubtitleTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER, 0, 6, 0, 0)); listView = new RecyclerListView(context) { @@ -639,44 +714,25 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. if (searchOpenPosition != -1 && !actionBar.isSearchFieldVisible()) { ignoreLayout = true; - layoutManager.scrollToPositionWithOffset(searchOpenPosition, searchOpenOffset); + layoutManager.scrollToPositionWithOffset(searchOpenPosition, searchOpenOffset - listView.getPaddingTop()); super.onLayout(false, l, t, r, b); ignoreLayout = false; searchOpenPosition = -1; } else if (scrollToSong) { scrollToSong = false; - boolean found = false; - MessageObject playingMessageObject = MediaController.getInstance().getPlayingMessageObject(); - if (playingMessageObject != null) { - int count = listView.getChildCount(); - for (int a = 0; a < count; a++) { - View child = listView.getChildAt(a); - if (child instanceof AudioPlayerCell) { - if (((AudioPlayerCell) child).getMessageObject() == playingMessageObject) { - if (child.getBottom() <= getMeasuredHeight()) { - found = true; - } - break; - } - } - } - if (!found) { - int idx = playlist.indexOf(playingMessageObject); - if (idx >= 0) { - ignoreLayout = true; - if (SharedConfig.playOrderReversed) { - layoutManager.scrollToPosition(idx); - } else { - layoutManager.scrollToPosition(playlist.size() - idx); - } - super.onLayout(false, l, t, r, b); - ignoreLayout = false; - } - } + ignoreLayout = true; + if (scrollToCurrentSong(true)) { + super.onLayout(false, l, t, r, b); } + ignoreLayout = false; } } + @Override + protected boolean allowSelectChildAtPosition(float x, float y) { + return y < playerLayout.getY() - listView.getTop(); + } + @Override public void requestLayout() { if (ignoreLayout) { @@ -684,22 +740,7 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. } super.requestLayout(); } - - @Override - protected boolean allowSelectChildAtPosition(float x, float y) { - return playerLayout == null || y > playerLayout.getY() + playerLayout.getMeasuredHeight(); - } - - @Override - public boolean drawChild(Canvas canvas, View child, long drawingTime) { - canvas.save(); - canvas.clipRect(0, (actionBar != null ? actionBar.getMeasuredHeight() : 0) + AndroidUtilities.dp(50), getMeasuredWidth(), getMeasuredHeight()); - boolean result = super.drawChild(canvas, child, drawingTime); - canvas.restore(); - return result; - } }; - listView.setPadding(0, 0, 0, AndroidUtilities.dp(8)); listView.setClipToPadding(false); listView.setLayoutManager(layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); listView.setHorizontalScrollBarEnabled(false); @@ -715,7 +756,17 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. listView.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + int offset = AndroidUtilities.dp(13); + int top = scrollOffsetY - backgroundPaddingTop - offset; + if (top + backgroundPaddingTop < ActionBar.getCurrentActionBarHeight() && listView.canScrollVertically(1)) { + View child = listView.getChildAt(0); + RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(0); + if (holder != null && holder.itemView.getTop() > AndroidUtilities.dp(7)) { + listView.smoothScrollBy(0, holder.itemView.getTop() - AndroidUtilities.dp(7)); + } + } + } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { AndroidUtilities.hideKeyboard(getCurrentFocus()); } } @@ -723,52 +774,152 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { updateLayout(); + updateEmptyViewPosition(); + + if (!searchWas) { + int firstVisibleItem = layoutManager.findFirstVisibleItemPosition(); + int visibleItemCount = firstVisibleItem == RecyclerView.NO_POSITION ? 0 : Math.abs(layoutManager.findLastVisibleItemPosition() - firstVisibleItem) + 1; + int totalItemCount = recyclerView.getAdapter().getItemCount(); + + MessageObject playingMessageObject = MediaController.getInstance().getPlayingMessageObject(); + if (SharedConfig.playOrderReversed) { + if (firstVisibleItem < 10) { + MediaController.getInstance().loadMoreMusic(); + } + } else { + if (firstVisibleItem + visibleItemCount > totalItemCount - 10) { + MediaController.getInstance().loadMoreMusic(); + } + } + } } }); playlist = MediaController.getInstance().getPlaylist(); listAdapter.notifyDataSetChanged(); - containerView.addView(playerLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 178)); - containerView.addView(shadow2, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3)); - containerView.addView(placeholderImageView, LayoutHelper.createFrame(40, 40, Gravity.TOP | Gravity.LEFT, 17, 19, 0, 0)); - containerView.addView(shadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3)); + containerView.addView(playerLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 179, Gravity.LEFT | Gravity.BOTTOM)); + containerView.addView(playerShadow, new FrameLayout.LayoutParams(LayoutHelper.MATCH_PARENT, AndroidUtilities.getShadowHeight(), Gravity.LEFT | Gravity.BOTTOM)); + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) playerShadow.getLayoutParams(); + layoutParams.bottomMargin = AndroidUtilities.dp(179); + containerView.addView(actionBarShadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3)); containerView.addView(actionBar); + blurredView = new FrameLayout(context) { + @Override + public boolean onTouchEvent(MotionEvent event) { + if (blurredView.getTag() != null) { + showAlbumCover(false, true); + } + return true; + } + }; + blurredView.setAlpha(0.0f); + blurredView.setVisibility(View.INVISIBLE); + getContainer().addView(blurredView); + + bigAlbumConver = new BackupImageView(context); + bigAlbumConver.setAspectFit(true); + bigAlbumConver.setRoundRadius(AndroidUtilities.dp(8)); + blurredView.addView(bigAlbumConver, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 30, 30, 30, 30)); + updateTitle(false); updateRepeatButton(); - updateShuffleButton(); + updateEmptyView(); } - @Keep - public void setFullAnimationProgress(float value) { - fullAnimationProgress = value; - placeholderImageView.setRoundRadius(AndroidUtilities.dp(20 * (1.0f - fullAnimationProgress))); - float scale = 1.0f + thumbMaxScale * fullAnimationProgress; - placeholderImageView.setScaleX(scale); - placeholderImageView.setScaleY(scale); - float translationY = placeholderImageView.getTranslationY(); - placeholderImageView.setTranslationX(thumbMaxX * fullAnimationProgress); - placeholderImageView.setTranslationY(startTranslation + (endTranslation - startTranslation) * fullAnimationProgress); - playerLayout.setTranslationY(panelStartTranslation + (panelEndTranslation - panelStartTranslation) * fullAnimationProgress); - shadow2.setTranslationY(panelStartTranslation + (panelEndTranslation - panelStartTranslation) * fullAnimationProgress + playerLayout.getMeasuredHeight()); - menuItem.setAlpha(fullAnimationProgress); - searchItem.setAlpha(1.0f - fullAnimationProgress); - avatarContainer.setAlpha(1.0f - fullAnimationProgress); - actionBar.getTitleTextView().setAlpha(fullAnimationProgress); - actionBar.getSubtitleTextView().setAlpha(fullAnimationProgress); + private void updateEmptyViewPosition() { + if (emptyView.getVisibility() != View.VISIBLE) { + return; + } + int h = playerLayout.getVisibility() == View.VISIBLE ? AndroidUtilities.dp(150) : -AndroidUtilities.dp(30); + emptyView.setTranslationY((emptyView.getMeasuredHeight() - containerView.getMeasuredHeight() - h) / 2); } - @Keep - public float getFullAnimationProgress() { - return fullAnimationProgress; + private void updateEmptyView() { + emptyView.setVisibility(searching && listAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + updateEmptyViewPosition(); + } + + private boolean scrollToCurrentSong(boolean search) { + MessageObject playingMessageObject = MediaController.getInstance().getPlayingMessageObject(); + if (playingMessageObject != null) { + boolean found = false; + if (search) { + int count = listView.getChildCount(); + for (int a = 0; a < count; a++) { + View child = listView.getChildAt(a); + if (child instanceof AudioPlayerCell) { + if (((AudioPlayerCell) child).getMessageObject() == playingMessageObject) { + if (child.getBottom() <= listView.getMeasuredHeight()) { + found = true; + } + break; + } + } + } + } + if (!found) { + int idx = playlist.indexOf(playingMessageObject); + if (idx >= 0) { + if (SharedConfig.playOrderReversed) { + layoutManager.scrollToPosition(idx); + } else { + layoutManager.scrollToPosition(playlist.size() - idx); + } + return true; + } + } + } + return false; + } + + @Override + public boolean onCustomMeasure(View view, int width, int height) { + boolean isPortrait = width < height; + if (view == blurredView) { + blurredView.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); + return true; + } + return false; + } + + @Override + protected boolean onCustomLayout(View view, int left, int top, int right, int bottom) { + int width = (right - left); + int height = (bottom - top); + boolean isPortrait = width < height; + if (view == blurredView) { + blurredView.layout(left, 0, left + width, height); + return true; + } + return false; + } + + private void setMenuItemChecked(ActionBarMenuSubItem item, boolean checked) { + if (checked) { + item.setTextColor(Theme.getColor(Theme.key_player_buttonActive)); + item.setIconColor(Theme.getColor(Theme.key_player_buttonActive)); + } else { + item.setTextColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem)); + item.setIconColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem)); + } + } + + private void updateSubMenu() { + setMenuItemChecked(shuffleListItem, SharedConfig.shuffleMusic); + setMenuItemChecked(reverseOrderItem, SharedConfig.playOrderReversed); + setMenuItemChecked(repeatListItem, SharedConfig.repeatMode == 1); + setMenuItemChecked(repeatSongItem, SharedConfig.repeatMode == 2); } private void updatePlaybackButton() { float currentPlaybackSpeed = MediaController.getInstance().getPlaybackSpeed(true); if (currentPlaybackSpeed > 1) { + playbackSpeedButton.setTag(Theme.key_inappPlayerPlayPause); playbackSpeedButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_inappPlayerPlayPause), PorterDuff.Mode.SRC_IN)); } else { + playbackSpeedButton.setTag(Theme.key_inappPlayerClose); playbackSpeedButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_inappPlayerClose), PorterDuff.Mode.SRC_IN)); } } @@ -895,18 +1046,77 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); parentActivity.presentFragment(new ChatActivity(args), false, false); dismiss(); + } else if (id == 5) { + if (Build.VERSION.SDK_INT >= 23 && parentActivity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + parentActivity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 4); + return; + } + String fileName = FileLoader.getDocumentFileName(messageObject.getDocument()); + if (TextUtils.isEmpty(fileName)) { + fileName = messageObject.getFileName(); + } + String path = messageObject.messageOwner.attachPath; + if (path != null && path.length() > 0) { + File temp = new File(path); + if (!temp.exists()) { + path = null; + } + } + if (path == null || path.length() == 0) { + path = FileLoader.getPathToMessage(messageObject.messageOwner).toString(); + } + MediaController.saveFile(path, parentActivity, 3, fileName, messageObject.getDocument() != null ? messageObject.getDocument().mime_type : ""); } } - private int getCurrentTop() { - if (listView.getChildCount() != 0) { - View child = listView.getChildAt(0); - RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findContainingViewHolder(child); - if (holder != null) { - return listView.getPaddingTop() - (holder.getAdapterPosition() == 0 && child.getTop() >= 0 ? child.getTop() : 0); + private void showAlbumCover(boolean show, boolean animated) { + if (show) { + if (blurredView.getVisibility() == View.VISIBLE || blurredAnimationInProgress) { + return; + } + blurredView.setTag(1); + bigAlbumConver.setImageBitmap(placeholderImageView.imageReceiver.getBitmap()); + blurredAnimationInProgress = true; + BaseFragment fragment = parentActivity.getActionBarLayout().fragmentsStack.get(parentActivity.getActionBarLayout().fragmentsStack.size() - 1); + View fragmentView = fragment.getFragmentView(); + int w = (int) (fragmentView.getMeasuredWidth() / 6.0f); + int h = (int) (fragmentView.getMeasuredHeight() / 6.0f); + Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.scale(1.0f / 6.0f, 1.0f / 6.0f); + fragmentView.draw(canvas); + canvas.translate(containerView.getLeft() - getLeftInset(), 0); + containerView.draw(canvas); + Utilities.stackBlurBitmap(bitmap, Math.max(7, Math.max(w, h) / 180)); + blurredView.setBackground(new BitmapDrawable(bitmap)); + blurredView.setVisibility(View.VISIBLE); + blurredView.animate().alpha(1.0f).setDuration(180).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + blurredAnimationInProgress = false; + } + }).start(); + } else { + if (blurredView.getVisibility() != View.VISIBLE) { + return; + } + blurredView.setTag(null); + if (animated) { + blurredAnimationInProgress = true; + blurredView.animate().alpha(0.0f).setDuration(180).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + blurredView.setVisibility(View.INVISIBLE); + bigAlbumConver.setImageBitmap(null); + blurredAnimationInProgress = false; + } + }).start(); + } else { + blurredView.setAlpha(0.0f); + blurredView.setVisibility(View.INVISIBLE); + bigAlbumConver.setImageBitmap(null); } } - return -1000; } @Override @@ -950,6 +1160,51 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. } else if (id == NotificationCenter.musicDidLoad) { playlist = MediaController.getInstance().getPlaylist(); listAdapter.notifyDataSetChanged(); + } else if (id == NotificationCenter.moreMusicDidLoad) { + playlist = MediaController.getInstance().getPlaylist(); + listAdapter.notifyDataSetChanged(); + if (SharedConfig.playOrderReversed) { + listView.stopScroll(); + int addedCount = (Integer) args[0]; + int firstVisibleItem = layoutManager.findFirstVisibleItemPosition(); + int position = layoutManager.findLastVisibleItemPosition(); + if (position != RecyclerView.NO_POSITION) { + View firstVisView = layoutManager.findViewByPosition(position); + int offset = firstVisView == null ? 0 : firstVisView.getTop(); + layoutManager.scrollToPositionWithOffset(position + addedCount, offset); + } + } + } else if (id == NotificationCenter.fileDidLoad) { + String name = (String) args[0]; + if (name.equals(currentFile)) { + updateTitle(false); + currentAudioFinishedLoading = true; + } + } else if (id == NotificationCenter.FileLoadProgressChanged) { + String name = (String) args[0]; + if (name.equals(currentFile)) { + MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); + if (messageObject == null) { + return; + } + Long loadedSize = (Long) args[1]; + Long totalSize = (Long) args[2]; + float bufferedProgress; + if (currentAudioFinishedLoading) { + bufferedProgress = 1.0f; + } else { + long newTime = SystemClock.elapsedRealtime(); + if (Math.abs(newTime - lastBufferedPositionCheck) >= 500) { + bufferedProgress = MediaController.getInstance().isStreamingCurrentAudio() ? FileLoader.getInstance(currentAccount).getBufferedProgressFromPosition(messageObject.audioProgress, currentFile) : 1.0f; + lastBufferedPositionCheck = newTime; + } else { + bufferedProgress = -1; + } + } + if (bufferedProgress != -1) { + seekBarView.setBufferedProgress(bufferedProgress); + } + } } } @@ -960,55 +1215,48 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. private void updateLayout() { if (listView.getChildCount() <= 0) { + listView.setTopGlowOffset(scrollOffsetY = listView.getPaddingTop()); + containerView.invalidate(); return; } View child = listView.getChildAt(0); RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findContainingViewHolder(child); int top = child.getTop(); - int newOffset = top > 0 && holder != null && holder.getAdapterPosition() == 0 ? top : 0; - if (searchWas || searching) { - newOffset = 0; + int newOffset = AndroidUtilities.dp(7); + if (top >= AndroidUtilities.dp(7) && holder != null && holder.getAdapterPosition() == 0) { + newOffset = top; } - if (scrollOffsetY != newOffset) { - listView.setTopGlowOffset(scrollOffsetY = newOffset); - playerLayout.setTranslationY(Math.max(actionBar.getMeasuredHeight(), scrollOffsetY)); - placeholderImageView.setTranslationY(Math.max(actionBar.getMeasuredHeight(), scrollOffsetY)); - shadow2.setTranslationY(Math.max(actionBar.getMeasuredHeight(), scrollOffsetY) + playerLayout.getMeasuredHeight()); - containerView.invalidate(); - - if (inFullSize && scrollOffsetY <= actionBar.getMeasuredHeight() || searchWas) { - if (actionBar.getTag() == null) { - if (actionBarAnimation != null) { - actionBarAnimation.cancel(); - } - actionBar.setTag(1); - actionBarAnimation = new AnimatorSet(); - actionBarAnimation.playTogether( - ObjectAnimator.ofFloat(actionBar, "alpha", 1.0f), - ObjectAnimator.ofFloat(shadow, "alpha", 1.0f), - ObjectAnimator.ofFloat(shadow2, "alpha", 1.0f)); - actionBarAnimation.setDuration(180); - actionBarAnimation.start(); - } - } else { - if (actionBar.getTag() != null) { - if (actionBarAnimation != null) { - actionBarAnimation.cancel(); - } - actionBar.setTag(null); - actionBarAnimation = new AnimatorSet(); - actionBarAnimation.playTogether( - ObjectAnimator.ofFloat(actionBar, "alpha", 0.0f), - ObjectAnimator.ofFloat(shadow, "alpha", 0.0f), - ObjectAnimator.ofFloat(shadow2, "alpha", 0.0f)); - actionBarAnimation.setDuration(180); - actionBarAnimation.start(); - } + boolean show = newOffset <= AndroidUtilities.dp(12); + if (show && actionBar.getTag() == null || !show && actionBar.getTag() != null) { + actionBar.setTag(show ? 1 : null); + if (actionBarAnimation != null) { + actionBarAnimation.cancel(); + actionBarAnimation = null; } - } + actionBarAnimation = new AnimatorSet(); + actionBarAnimation.setDuration(180); + actionBarAnimation.playTogether( + ObjectAnimator.ofFloat(actionBar, View.ALPHA, show ? 1.0f : 0.0f), + ObjectAnimator.ofFloat(actionBarShadow, View.ALPHA, show ? 1.0f : 0.0f)); + actionBarAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { - startTranslation = Math.max(actionBar.getMeasuredHeight(), scrollOffsetY); - panelStartTranslation = Math.max(actionBar.getMeasuredHeight(), scrollOffsetY); + } + + @Override + public void onAnimationCancel(Animator animation) { + actionBarAnimation = null; + } + }); + actionBarAnimation.start(); + } + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); + newOffset += layoutParams.topMargin - AndroidUtilities.dp(11); + if (scrollOffsetY != newOffset) { + listView.setTopGlowOffset((scrollOffsetY = newOffset) - layoutParams.topMargin); + containerView.invalidate(); + } } @Override @@ -1018,7 +1266,10 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.messagePlayingPlayStateChanged); NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.messagePlayingDidStart); NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.messagePlayingProgressDidChanged); + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.fileDidLoad); + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.FileLoadProgressChanged); NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.musicDidLoad); + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.moreMusicDidLoad); DownloadController.getInstance(currentAccount).removeLoadingFileObserver(this); } @@ -1028,6 +1279,10 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. actionBar.closeSearchField(); return; } + if (blurredView.getTag() != null) { + showAlbumCover(false, true); + return; + } super.onBackPressed(); } @@ -1056,43 +1311,48 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. return TAG; } - private void updateShuffleButton() { - if (SharedConfig.shuffleMusic) { - Drawable drawable = getContext().getResources().getDrawable(R.drawable.pl_shuffle).mutate(); - drawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_buttonActive), PorterDuff.Mode.SRC_IN)); - shuffleButton.setIcon(drawable); - shuffleButton.setContentDescription(LocaleController.getString("Shuffle", R.string.Shuffle)); - } else { - Drawable drawable = getContext().getResources().getDrawable(R.drawable.music_reverse).mutate(); - if (SharedConfig.playOrderReversed) { - drawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_buttonActive), PorterDuff.Mode.SRC_IN)); - } else { - drawable.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_button), PorterDuff.Mode.SRC_IN)); - } - shuffleButton.setIcon(drawable); - shuffleButton.setContentDescription(LocaleController.getString("ReverseOrder", R.string.ReverseOrder)); - } - - playOrderButtons[0].setColorFilter(new PorterDuffColorFilter(Theme.getColor(SharedConfig.playOrderReversed ? Theme.key_player_buttonActive : Theme.key_player_button), PorterDuff.Mode.SRC_IN)); - playOrderButtons[1].setColorFilter(new PorterDuffColorFilter(Theme.getColor(SharedConfig.shuffleMusic ? Theme.key_player_buttonActive : Theme.key_player_button), PorterDuff.Mode.SRC_IN)); - } - private void updateRepeatButton() { int mode = SharedConfig.repeatMode; - if (mode == 0) { - repeatButton.setImageResource(R.drawable.pl_repeat); - repeatButton.setTag(Theme.key_player_button); - repeatButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_button), PorterDuff.Mode.SRC_IN)); - repeatButton.setContentDescription(LocaleController.getString("AccDescrRepeatOff", R.string.AccDescrRepeatOff)); - } else if (mode == 1) { - repeatButton.setImageResource(R.drawable.pl_repeat); - repeatButton.setTag(Theme.key_player_buttonActive); - repeatButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_buttonActive), PorterDuff.Mode.SRC_IN)); - repeatButton.setContentDescription(LocaleController.getString("AccDescrRepeatList", R.string.AccDescrRepeatList)); + if (mode == 0 || mode == 1) { + if (SharedConfig.shuffleMusic) { + if (mode == 0) { + repeatButton.setIcon(R.drawable.player_new_shuffle); + } else { + repeatButton.setIcon(R.drawable.player_new_repeat_shuffle); + } + } else if (SharedConfig.playOrderReversed) { + if (mode == 0) { + repeatButton.setIcon(R.drawable.player_new_order); + } else { + repeatButton.setIcon(R.drawable.player_new_repeat_reverse); + } + } else { + repeatButton.setIcon(R.drawable.player_new_repeatall); + } + if (mode == 0 && !SharedConfig.shuffleMusic && !SharedConfig.playOrderReversed) { + repeatButton.setTag(Theme.key_player_button); + repeatButton.setIconColor(Theme.getColor(Theme.key_player_button)); + Theme.setSelectorDrawableColor(repeatButton.getBackground(), Theme.getColor(Theme.key_listSelector), true); + repeatButton.setContentDescription(LocaleController.getString("AccDescrRepeatOff", R.string.AccDescrRepeatOff)); + } else { + repeatButton.setTag(Theme.key_player_buttonActive); + repeatButton.setIconColor(Theme.getColor(Theme.key_player_buttonActive)); + Theme.setSelectorDrawableColor(repeatButton.getBackground(), Theme.getColor(Theme.key_player_buttonActive) & 0x19ffffff, true); + if (mode == 0) { + if (SharedConfig.shuffleMusic) { + repeatButton.setContentDescription(LocaleController.getString("ShuffleList", R.string.ShuffleList)); + } else { + repeatButton.setContentDescription(LocaleController.getString("ReverseOrder", R.string.ReverseOrder)); + } + } else { + repeatButton.setContentDescription(LocaleController.getString("AccDescrRepeatList", R.string.AccDescrRepeatList)); + } + } } else if (mode == 2) { - repeatButton.setImageResource(R.drawable.pl_repeat1); + repeatButton.setIcon(R.drawable.player_new_repeatone); repeatButton.setTag(Theme.key_player_buttonActive); - repeatButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_player_buttonActive), PorterDuff.Mode.SRC_IN)); + repeatButton.setIconColor(Theme.getColor(Theme.key_player_buttonActive)); + Theme.setSelectorDrawableColor(repeatButton.getBackground(), Theme.getColor(Theme.key_player_buttonActive) & 0x19ffffff, true); repeatButton.setContentDescription(LocaleController.getString("AccDescrRepeatOne", R.string.AccDescrRepeatOne)); } } @@ -1104,7 +1364,22 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. newTime = (int) (messageObject.getDuration() * seekBarView.getProgress()); } else { seekBarView.setProgress(messageObject.audioProgress); - seekBarView.setBufferedProgress(messageObject.bufferedProgress); + + float bufferedProgress; + if (currentAudioFinishedLoading) { + bufferedProgress = 1.0f; + } else { + long time = SystemClock.elapsedRealtime(); + if (Math.abs(time - lastBufferedPositionCheck) >= 500) { + bufferedProgress = MediaController.getInstance().isStreamingCurrentAudio() ? FileLoader.getInstance(currentAccount).getBufferedProgressFromPosition(messageObject.audioProgress, currentFile) : 1.0f; + lastBufferedPositionCheck = time; + } else { + bufferedProgress = -1; + } + } + if (bufferedProgress != -1) { + seekBarView.setBufferedProgress(bufferedProgress); + } newTime = messageObject.audioProgressSec; } if (lastTime != newTime) { @@ -1151,51 +1426,56 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. return; } if (messageObject.eventId != 0 || messageObject.getId() <= -2000000000) { - hasOptions = false; - menuItem.setVisibility(View.INVISIBLE); optionsButton.setVisibility(View.INVISIBLE); } else { - hasOptions = true; - if (!actionBar.isSearchFieldVisible()) { - menuItem.setVisibility(View.VISIBLE); - } optionsButton.setVisibility(View.VISIBLE); } checkIfMusicDownloaded(messageObject); updateProgress(messageObject); if (MediaController.getInstance().isMessagePaused()) { - playButton.setImageDrawable(Theme.createSimpleSelectorDrawable(playButton.getContext(), R.drawable.pl_play, Theme.getColor(Theme.key_player_button), Theme.getColor(Theme.key_player_buttonActive))); + playButton.setImageResource(R.drawable.player_new_play); playButton.setContentDescription(LocaleController.getString("AccActionPlay", R.string.AccActionPlay)); } else { - playButton.setImageDrawable(Theme.createSimpleSelectorDrawable(playButton.getContext(), R.drawable.pl_pause, Theme.getColor(Theme.key_player_button), Theme.getColor(Theme.key_player_buttonActive))); + playButton.setImageResource(R.drawable.player_new_pause); playButton.setContentDescription(LocaleController.getString("AccActionPause", R.string.AccActionPause)); } String title = messageObject.getMusicTitle(); String author = messageObject.getMusicAuthor(); titleTextView.setText(title); authorTextView.setText(author); - actionBar.setTitle(title); - actionBar.setSubtitle(author); String loadTitle = author + " " + title; AudioInfo audioInfo = MediaController.getInstance().getAudioInfo(); if (audioInfo != null && audioInfo.getCover() != null) { - hasNoCover = 0; placeholderImageView.setImageBitmap(audioInfo.getCover()); + currentFile = null; + currentAudioFinishedLoading = true; } else { + TLRPC.Document document = messageObject.getDocument(); + currentFile = FileLoader.getAttachFileName(document); + currentAudioFinishedLoading = false; + TLRPC.PhotoSize thumb = document != null ? FileLoader.getClosestPhotoSizeWithSize(document.thumbs, 240) : null; + if (!(thumb instanceof TLRPC.TL_photoSize)) { + thumb = null; + } String artworkUrl = messageObject.getArtworkUrl(false); if (!TextUtils.isEmpty(artworkUrl)) { - placeholderImageView.setImage(artworkUrl, null, null); - hasNoCover = 2; + if (thumb != null) { + placeholderImageView.setImage(ImageLocation.getForPath(artworkUrl), null, ImageLocation.getForDocument(thumb, document), null, null, 0, 1, messageObject); + } else { + placeholderImageView.setImage(artworkUrl, null, null); + } + } else if (thumb != null) { + placeholderImageView.setImage(null, null, ImageLocation.getForDocument(thumb, document), null, null, 0, 1, messageObject); } else { placeholderImageView.setImageDrawable(null); - hasNoCover = 1; } placeholderImageView.invalidate(); } - int duration = messageObject.getDuration(); + int duration = lastDuration = messageObject.getDuration(); + if (durationTextView != null) { durationTextView.setText(duration != 0 ? AndroidUtilities.formatShortDuration(duration) : "-:--"); } @@ -1212,101 +1492,78 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. private Context context; private ArrayList searchResult = new ArrayList<>(); - private Timer searchTimer; + private Runnable searchRunnable; public ListAdapter(Context context) { this.context = context; } + @Override + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + if (playlist.size() > 1) { + playerLayout.setBackgroundColor(Theme.getColor(Theme.key_player_background)); + playerShadow.setVisibility(View.VISIBLE); + listView.setPadding(0, listView.getPaddingTop(), 0, AndroidUtilities.dp(179)); + } else { + playerLayout.setBackground(null); + playerShadow.setVisibility(View.INVISIBLE); + listView.setPadding(0, listView.getPaddingTop(), 0, 0); + } + updateEmptyView(); + } + @Override public int getItemCount() { if (searchWas) { return searchResult.size(); - } else if (searching) { - return playlist.size(); } - return 1 + playlist.size(); + return playlist.size() > 1 ? playlist.size() : 0; } @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { - return searchWas || holder.getAdapterPosition() > 0; + return true; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view; - switch (viewType) { - case 0: - view = new View(context); - view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, AndroidUtilities.dp(178))); - break; - case 1: - default: - view = new AudioPlayerCell(context); - break; - } + View view = new AudioPlayerCell(context); return new RecyclerListView.Holder(view); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - if (holder.getItemViewType() == 1) { - AudioPlayerCell cell = (AudioPlayerCell) holder.itemView; - if (searchWas) { - cell.setMessageObject(searchResult.get(position)); - } else if (searching) { - if (SharedConfig.playOrderReversed) { - cell.setMessageObject(playlist.get(position)); - } else { - cell.setMessageObject(playlist.get(playlist.size() - position - 1)); - } - } else if (position > 0) { - if (SharedConfig.playOrderReversed) { - cell.setMessageObject(playlist.get(position - 1)); - } else { - cell.setMessageObject(playlist.get(playlist.size() - position)); - } + AudioPlayerCell cell = (AudioPlayerCell) holder.itemView; + if (searchWas) { + cell.setMessageObject(searchResult.get(position)); + } else { + if (SharedConfig.playOrderReversed) { + cell.setMessageObject(playlist.get(position)); + } else { + cell.setMessageObject(playlist.get(playlist.size() - position - 1)); } } } @Override public int getItemViewType(int i) { - if (searchWas || searching) { - return 1; - } - if (i == 0) { - return 0; - } - return 1; + return 0; } public void search(final String query) { - try { - if (searchTimer != null) { - searchTimer.cancel(); - } - } catch (Exception e) { - FileLog.e(e); + if (searchRunnable != null) { + Utilities.searchQueue.cancelRunnable(searchRunnable); + searchRunnable = null; } if (query == null) { searchResult.clear(); notifyDataSetChanged(); } else { - searchTimer = new Timer(); - searchTimer.schedule(new TimerTask() { - @Override - public void run() { - try { - searchTimer.cancel(); - searchTimer = null; - } catch (Exception e) { - FileLog.e(e); - } - processSearch(query); - } - }, 200, 300); + Utilities.searchQueue.postRunnable(searchRunnable = () -> { + searchRunnable = null; + processSearch(query); + }, 300); } } @@ -1316,7 +1573,7 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. Utilities.searchQueue.postRunnable(() -> { String search1 = query.trim().toLowerCase(); if (search1.length() == 0) { - updateSearchResults(new ArrayList<>()); + updateSearchResults(new ArrayList<>(), query); return; } String search2 = LocaleController.getInstance().getTranslitString(search1); @@ -1370,12 +1627,12 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. } } - updateSearchResults(resultArray); + updateSearchResults(resultArray, query); }); }); } - private void updateSearchResults(final ArrayList documents) { + private void updateSearchResults(final ArrayList documents, String query) { AndroidUtilities.runOnUIThread(() -> { if (!searching) { return; @@ -1384,7 +1641,106 @@ public class AudioPlayerAlert extends BottomSheet implements NotificationCenter. searchResult = documents; notifyDataSetChanged(); layoutManager.scrollToPosition(0); + emptySubtitleTextView.setText(AndroidUtilities.replaceTags(LocaleController.formatString("NoAudioFoundPlayerInfo", R.string.NoAudioFoundPlayerInfo, query))); }); } } + + @Override + public ArrayList getThemeDescriptions() { + ArrayList themeDescriptions = new ArrayList<>(); + + ThemeDescription.ThemeDescriptionDelegate delegate = () -> { + EditTextBoldCursor editText = searchItem.getSearchField(); + editText.setCursorColor(Theme.getColor(Theme.key_player_actionBarTitle)); + + repeatButton.setIconColor(Theme.getColor((String) repeatButton.getTag())); + Theme.setSelectorDrawableColor(repeatButton.getBackground(), Theme.getColor(Theme.key_listSelector), true); + + optionsButton.setIconColor(Theme.getColor(Theme.key_player_button)); + Theme.setSelectorDrawableColor(optionsButton.getBackground(), Theme.getColor(Theme.key_listSelector), true); + + progressView.setBackgroundColor(Theme.getColor(Theme.key_player_progressBackground)); + progressView.setProgressColor(Theme.getColor(Theme.key_player_progress)); + + updateSubMenu(); + repeatButton.redrawPopup(Theme.getColor(Theme.key_actionBarDefaultSubmenuBackground)); + + optionsButton.setPopupItemsColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem), false); + optionsButton.setPopupItemsColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem), true); + optionsButton.redrawPopup(Theme.getColor(Theme.key_actionBarDefaultSubmenuBackground)); + }; + + themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_player_actionBar)); + themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, delegate, Theme.key_player_actionBarTitle)); + themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_player_actionBarTitle)); + themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBTITLECOLOR, null, null, null, null, Theme.key_player_actionBarTitle)); + themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_player_actionBarSelector)); + themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCH, null, null, null, null, Theme.key_player_actionBarTitle)); + themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCHPLACEHOLDER, null, null, null, null, Theme.key_player_time)); + + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{AudioPlayerCell.class}, null, null, null, Theme.key_chat_inLoader)); + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{AudioPlayerCell.class}, null, null, null, Theme.key_chat_outLoader)); + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{AudioPlayerCell.class}, null, null, null, Theme.key_chat_inLoaderSelected)); + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{AudioPlayerCell.class}, null, null, null, Theme.key_chat_inMediaIcon)); + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{AudioPlayerCell.class}, null, null, null, Theme.key_chat_inMediaIconSelected)); + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{AudioPlayerCell.class}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2)); + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{AudioPlayerCell.class}, null, null, null, Theme.key_chat_inAudioSelectedProgress)); + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{AudioPlayerCell.class}, null, null, null, Theme.key_chat_inAudioProgress)); + + themeDescriptions.add(new ThemeDescription(containerView, 0, null, null, new Drawable[]{shadowDrawable}, null, Theme.key_dialogBackground)); + + themeDescriptions.add(new ThemeDescription(progressView, 0, null, null, null, null, Theme.key_player_progressBackground)); + themeDescriptions.add(new ThemeDescription(progressView, 0, null, null, null, null, Theme.key_player_progress)); + themeDescriptions.add(new ThemeDescription(seekBarView, 0, null, null, null, null, Theme.key_player_progressBackground)); + themeDescriptions.add(new ThemeDescription(seekBarView, 0, null, null, null, null, Theme.key_player_progress)); + themeDescriptions.add(new ThemeDescription(seekBarView, 0, null, null, null, null, Theme.key_player_progressCachedBackground)); + + themeDescriptions.add(new ThemeDescription(playbackSpeedButton, ThemeDescription.FLAG_CHECKTAG | ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_inappPlayerPlayPause)); + themeDescriptions.add(new ThemeDescription(playbackSpeedButton, ThemeDescription.FLAG_CHECKTAG | ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_inappPlayerClose)); + + themeDescriptions.add(new ThemeDescription(repeatButton, 0, null, null, null, delegate, Theme.key_player_button)); + themeDescriptions.add(new ThemeDescription(repeatButton, 0, null, null, null, delegate, Theme.key_player_buttonActive)); + themeDescriptions.add(new ThemeDescription(repeatButton, 0, null, null, null, delegate, Theme.key_listSelector)); + themeDescriptions.add(new ThemeDescription(repeatButton, 0, null, null, null, delegate, Theme.key_actionBarDefaultSubmenuItem)); + themeDescriptions.add(new ThemeDescription(repeatButton, 0, null, null, null, delegate, Theme.key_actionBarDefaultSubmenuBackground)); + themeDescriptions.add(new ThemeDescription(optionsButton, 0, null, null, null, delegate, Theme.key_player_button)); + themeDescriptions.add(new ThemeDescription(optionsButton, 0, null, null, null, delegate, Theme.key_listSelector)); + themeDescriptions.add(new ThemeDescription(optionsButton, 0, null, null, null, delegate, Theme.key_actionBarDefaultSubmenuItem)); + themeDescriptions.add(new ThemeDescription(optionsButton, 0, null, null, null, delegate, Theme.key_actionBarDefaultSubmenuBackground)); + + themeDescriptions.add(new ThemeDescription(prevButton, ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_player_button)); + themeDescriptions.add(new ThemeDescription(prevButton, ThemeDescription.FLAG_IMAGECOLOR | ThemeDescription.FLAG_USEBACKGROUNDDRAWABLE, null, null, null, null, Theme.key_listSelector)); + + themeDescriptions.add(new ThemeDescription(playButton, ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_player_button)); + themeDescriptions.add(new ThemeDescription(playButton, ThemeDescription.FLAG_IMAGECOLOR | ThemeDescription.FLAG_USEBACKGROUNDDRAWABLE, null, null, null, null, Theme.key_listSelector)); + + themeDescriptions.add(new ThemeDescription(nextButton, ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_player_button)); + themeDescriptions.add(new ThemeDescription(nextButton, ThemeDescription.FLAG_IMAGECOLOR | ThemeDescription.FLAG_USEBACKGROUNDDRAWABLE, null, null, null, null, Theme.key_listSelector)); + + themeDescriptions.add(new ThemeDescription(playerLayout, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_player_background)); + + themeDescriptions.add(new ThemeDescription(playerShadow, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_dialogShadowLine)); + + themeDescriptions.add(new ThemeDescription(emptyImageView, ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_dialogEmptyImage)); + themeDescriptions.add(new ThemeDescription(emptyTitleTextView, ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_dialogEmptyText)); + themeDescriptions.add(new ThemeDescription(emptySubtitleTextView, ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_dialogEmptyText)); + + themeDescriptions.add(new ThemeDescription(listView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_dialogScrollGlow)); + + themeDescriptions.add(new ThemeDescription(listView, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector)); + themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); + + themeDescriptions.add(new ThemeDescription(progressView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_emptyListPlaceholder)); + themeDescriptions.add(new ThemeDescription(progressView, ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_progressCircle)); + + themeDescriptions.add(new ThemeDescription(durationTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_player_time)); + themeDescriptions.add(new ThemeDescription(timeTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_player_time)); + themeDescriptions.add(new ThemeDescription(titleTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_player_actionBarTitle)); + themeDescriptions.add(new ThemeDescription(authorTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_player_time)); + + themeDescriptions.add(new ThemeDescription(containerView, 0, null, null, null, null, Theme.key_sheet_scrollUp)); + + return themeDescriptions; + } } \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/BackupImageView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/BackupImageView.java index 864514d35..29f5a630d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/BackupImageView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/BackupImageView.java @@ -48,6 +48,10 @@ public class BackupImageView extends View { setImage(imageLocation, imageFilter, null, null, thumb, null, null, 0, parentObject); } + public void setImage(ImageLocation mediaLocation, String mediaFilter, ImageLocation imageLocation, String imageFilter, Drawable thumb, Object parentObject) { + imageReceiver.setImage(mediaLocation, mediaFilter, imageLocation, imageFilter, null, null, thumb, 0, null, parentObject, 1); + } + public void setImage(ImageLocation imageLocation, String imageFilter, Bitmap thumb, Object parentObject) { setImage(imageLocation, imageFilter, null, null, null, thumb, null, 0, parentObject); } @@ -64,6 +68,14 @@ public class BackupImageView extends View { imageReceiver.setImage(imageLocation, imageFilter, null, null, thumb, size, null, parentObject, cacheType); } + public void setImageMedia(ImageLocation mediaLocation, String mediaFilter, ImageLocation imageLocation, String imageFilter, Bitmap thumbBitmap, int size, int cacheType, Object parentObject) { + Drawable thumb = null; + if (thumbBitmap != null) { + thumb = new BitmapDrawable(null, thumbBitmap); + } + imageReceiver.setImage(mediaLocation, mediaFilter, imageLocation, imageFilter, null, null, thumb, size, null, parentObject, cacheType); + } + public void setImage(ImageLocation imageLocation, String imageFilter, ImageLocation thumbLocation, String thumbFilter, int size, Object parentObject) { setImage(imageLocation, imageFilter, thumbLocation, thumbFilter, null, null, null, size, parentObject); } @@ -87,6 +99,10 @@ public class BackupImageView extends View { imageReceiver.setImage(imageLocation, imageFilter, thumbLocation, thumbFilter, null, size, ext, parentObject, cacheType); } + public void setImageMedia(ImageLocation mediaLocation, String mediaFilter, ImageLocation imageLocation, String imageFilter, ImageLocation thumbLocation, String thumbFilter, String ext, int size, int cacheType, Object parentObject) { + imageReceiver.setImage(mediaLocation, mediaFilter, imageLocation, imageFilter, thumbLocation, thumbFilter, null, size, ext, parentObject, cacheType); + } + public void setImageBitmap(Bitmap bitmap) { imageReceiver.setImageBitmap(bitmap); } 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 6ad0312de..d7c9e7c56 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java @@ -223,6 +223,8 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private AccountInstance accountInstance = AccountInstance.getInstance(UserConfig.selectedAccount); private SeekBarWaveform seekBarWaveform; + private boolean isInitLineCount; + private int lineCount = 1; private class SeekBarWaveformView extends View { @@ -288,7 +290,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } }; - private EditTextCaption messageEditText; + protected EditTextCaption messageEditText; private SimpleTextView slowModeButton; private int slowModeTimer; private Runnable updateSlowModeRunnable; @@ -438,6 +440,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe private boolean waitingForKeyboardOpen; private boolean wasSendTyping; + protected boolean shouldAnimateEditTextWithBounds; private Runnable openKeyboardRunnable = new Runnable() { @Override public void run() { @@ -1200,7 +1203,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } canvas.save(); float s = (1f - progressToSeekbarStep2) * (1f - exitProgress2); - canvas.scale(s, s, sendRect.centerX(), sendRect.centerY()); + //canvas.scale(s, s, sendRect.centerX(), sendRect.centerY()); //canvas.clipPath(new Path()); float a = /*(canceledByGesture ? (1f - slideToCancelProgress) : 1) */ (1f - exitProgress2); drawIcon(canvas, drawable, replaceDrawable, progressToSendButton, (int) ((1f - progressToSeekbarStep2) * a * 255)); @@ -1861,6 +1864,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe setFocusable(true); setFocusableInTouchMode(true); setWillNotDraw(false); + setClipChildren(false); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.recordStarted); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.recordStartError); @@ -1904,6 +1908,18 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe scheduledButton.layout(x, scheduledButton.getTop(), x + scheduledButton.getMeasuredWidth(), scheduledButton.getBottom()); } } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (child == messageEditText) { + canvas.save(); + canvas.clipRect(0, -getTop() - textFieldContainer.getTop() - ChatActivityEnterView.this.getTop(), getMeasuredWidth(), getMeasuredHeight() - AndroidUtilities.dp(6)); + boolean rez = super.drawChild(canvas, child, drawingTime); + canvas.restore(); + return rez; + } + return super.drawChild(canvas, child, drawingTime); + } }; frameLayout.setClipChildren(false); textFieldContainer.addView(frameLayout, LayoutHelper.createLinear(0, LayoutHelper.WRAP_CONTENT, 1.0f, Gravity.BOTTOM)); @@ -2034,6 +2050,15 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe return super.requestRectangleOnScreen(rectangle); } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + isInitLineCount = getMeasuredWidth() == 0 && getMeasuredHeight() == 0; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (isInitLineCount) { + lineCount = getLineCount(); + } + isInitLineCount = false; + } }; messageEditText.setDelegate(new EditTextCaption.EditTextCaptionDelegate() { @@ -2135,7 +2160,9 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } }); messageEditText.addTextChangedListener(new TextWatcher() { - boolean processChange = false; + + private boolean processChange; + private boolean nextChangeIsSend; @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { @@ -2144,9 +2171,19 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe @Override public void onTextChanged(CharSequence charSequence, int start, int before, int count) { + if (lineCount != messageEditText.getLineCount()) { + if (!isInitLineCount && messageEditText.getMeasuredWidth() > 0) { + onLineCountChanged(lineCount, messageEditText.getLineCount()); + } + lineCount = messageEditText.getLineCount(); + } + if (innerTextChange == 1) { return; } + if (sendByEnter && editingMessageObject == null && count > before && charSequence.length() > 0 && count == 1 && before == 0 && charSequence.length() == start + count && charSequence.charAt(charSequence.length() - 1) == '\n') { + nextChangeIsSend = true; + } checkSendButton(true); CharSequence message = AndroidUtilities.getTrimmedString(charSequence.toString()); if (delegate != null) { @@ -2181,8 +2218,9 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe if (innerTextChange != 0) { return; } - if (sendByEnter && editable.length() > 0 && editable.charAt(editable.length() - 1) == '\n' && editingMessageObject == null) { + if (nextChangeIsSend) { sendMessage(); + nextChangeIsSend = false; } if (processChange) { ImageSpan[] spans = editable.getSpans(0, editable.length(), ImageSpan.class); @@ -2773,6 +2811,14 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe sendButtonInverseDrawable.draw(canvas); } } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (getAlpha() <= 0f) { // for accessibility + return false; + } + return super.onTouchEvent(event); + } }; sendButton.setVisibility(INVISIBLE); int color = Theme.getColor(Theme.key_chat_messagePanelSend); @@ -2816,7 +2862,15 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe return onSendLongClick(v); }); - expandStickersButton = new ImageView(context); + expandStickersButton = new ImageView(context) { + @Override + public boolean onTouchEvent(MotionEvent event) { + if (getAlpha() <= 0f) { // for accessibility + return false; + } + return super.onTouchEvent(event); + } + }; expandStickersButton.setScaleType(ImageView.ScaleType.CENTER); expandStickersButton.setImageDrawable(stickersArrow = new AnimatedArrowDrawable(Theme.getColor(Theme.key_chat_messagePanelIcons), false)); expandStickersButton.setVisibility(GONE); @@ -2883,6 +2937,10 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe checkChannelRights(); } + protected void onLineCountChanged(int oldLineCount, int newLineCount) { + + } + private void startLockTransition() { AnimatorSet animatorSet = new AnimatorSet(); if (!NekoConfig.disableVibration) { @@ -2918,12 +2976,21 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - if (child == topView) { + boolean clip = child == topView || child == textFieldContainer; + if (clip) { canvas.save(); + if (child == textFieldContainer) { + int top = animatedTop + AndroidUtilities.dp(2); + if (topView != null && topView.getVisibility() == View.VISIBLE) { + top += topView.getHeight(); + } + canvas.clipRect(0, top, getMeasuredWidth(), getMeasuredHeight()); + } else { canvas.clipRect(0, animatedTop, getMeasuredWidth(), animatedTop + child.getLayoutParams().height + AndroidUtilities.dp(2)); } + } boolean result = super.drawChild(canvas, child, drawingTime); - if (child == topView) { + if (clip) { canvas.restore(); } return result; @@ -3055,7 +3122,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe AlertsCreator.createScheduleDatePickerDialog(parentActivity, parentFragment.getDialogId(), this::sendMessageInternal); }); cell.setMinimumWidth(AndroidUtilities.dp(196)); - sendPopupLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a++, 0, 0)); + sendPopupLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); } @@ -3068,7 +3135,9 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe AlertsCreator.createScheduleDatePickerDialog(parentActivity, parentFragment.getDialogId(), this::sendMessageInternal); }); cell.setMinimumWidth(AndroidUtilities.dp(196)); - sendPopupLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a++, 0, 0)); + sendPopupLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); + + sendPopupLayout.setupRadialSelectors(Theme.getColor(Theme.key_dialogButtonSelector)); sendPopupWindow = new ActionBarPopupWindow(sendPopupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT) { @Override @@ -5001,6 +5070,14 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe boolean shouldShowFastTransition = false; if (runningAnimationAudio != null) { shouldShowFastTransition = runningAnimationAudio.isRunning(); + if (videoSendButton != null) { + videoSendButton.setScaleX(1f); + videoSendButton.setScaleY(1f); + } + if (audioSendButton != null) { + audioSendButton.setScaleX(1f); + audioSendButton.setScaleY(1f); + } runningAnimationAudio.removeAllListeners(); runningAnimationAudio.cancel(); } @@ -5037,9 +5114,13 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe ObjectAnimator.ofFloat(recordCircle, "slideToCancelProgress", 1f) ); if (audioSendButton != null) { + audioSendButton.setScaleX(1f); + audioSendButton.setScaleY(1f); runningAnimationAudio.playTogether(ObjectAnimator.ofFloat(audioSendButton, View.ALPHA, isInVideoMode() ? 0 : 1)); } if (videoSendButton != null) { + videoSendButton.setScaleX(1f); + videoSendButton.setScaleY(1f); runningAnimationAudio.playTogether(ObjectAnimator.ofFloat(videoSendButton, View.ALPHA, isInVideoMode() ? 1 : 0)); } if (scheduledButton != null) { @@ -5159,6 +5240,20 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe ); } + iconsAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (videoSendButton != null) { + videoSendButton.setScaleX(1f); + videoSendButton.setScaleY(1f); + } + if (audioSendButton != null) { + audioSendButton.setScaleX(1f); + audioSendButton.setScaleY(1f); + } + } + }); + iconsAnimator.setDuration(150); iconsAnimator.setStartDelay(150); @@ -5198,6 +5293,12 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe recordedAudioPlayButton.setScaleY(1f); recordedAudioPlayButton.setScaleX(1f); recordedAudioSeekBar.setAlpha(1f); + + for (int i = 0; i < 2; i++) { + emojiButton[i].setScaleY(0f); + emojiButton[i].setScaleX(0f); + emojiButton[i].setAlpha(0f); + } } }); @@ -5232,14 +5333,14 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe audioVideoButtonContainer.setScaleX(0); audioVideoButtonContainer.setScaleY(0); - if (attachButton.getVisibility() == View.VISIBLE) { + if (attachButton != null && attachButton.getVisibility() == View.VISIBLE) { attachButton.setScaleX(0); attachButton.setScaleY(0); } - if (botButton.getVisibility() == View.VISIBLE) { - attachButton.setScaleX(0); - attachButton.setScaleY(0); + if (botButton != null && botButton.getVisibility() == View.VISIBLE) { + botButton.setScaleX(0); + botButton.setScaleY(0); } iconsAnimator.playTogether( @@ -5373,9 +5474,13 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe ObjectAnimator.ofFloat(audioVideoButtonContainer, View.ALPHA, 1.0f) ); if (audioSendButton != null) { + audioSendButton.setScaleX(1f); + audioSendButton.setScaleY(1f); iconsAnimator.playTogether(ObjectAnimator.ofFloat(audioSendButton, View.ALPHA, isInVideoMode() ? 0 : 1)); } if (videoSendButton != null) { + videoSendButton.setScaleX(1f); + videoSendButton.setScaleY(1f); iconsAnimator.playTogether(ObjectAnimator.ofFloat(videoSendButton, View.ALPHA, isInVideoMode() ? 1 : 0)); } if (attachLayout != null) { @@ -7627,6 +7732,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe String oldString; long startTime; long stopTime; + long lastSendTypingTime; SpannableStringBuilder replaceIn = new SpannableStringBuilder(); SpannableStringBuilder replaceOut = new SpannableStringBuilder(); @@ -7652,6 +7758,7 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe public void start() { isRunning = true; startTime = System.currentTimeMillis(); + lastSendTypingTime = startTime; invalidate(); } @@ -7663,12 +7770,14 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } invalidate(); } + lastSendTypingTime = 0; } @SuppressLint("DrawAllocation") @Override protected void onDraw(Canvas canvas) { - long t = isRunning ? (System.currentTimeMillis() - startTime) : stopTime - startTime; + long currentTimeMillis = System.currentTimeMillis(); + long t = isRunning ? (currentTimeMillis - startTime) : stopTime - startTime; long time = t / 1000; int ms = (int) (t % 1000L) / 10; @@ -7680,6 +7789,11 @@ public class ChatActivityEnterView extends FrameLayout implements NotificationCe } } + if (isRunning && currentTimeMillis > lastSendTypingTime + 5000) { + lastSendTypingTime = currentTimeMillis; + MessagesController.getInstance(currentAccount).sendTyping(dialog_id, videoSendButton != null && videoSendButton.getTag() != null ? 7 : 1, 0); + } + String newString; if (time / 60 >= 60) { newString = String.format(Locale.US, "%01d:%02d:%02d,%d", (time / 60) / 60, (time / 60) % 60, time % 60, ms / 10); 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 bdc7943f4..2d8ad3ed9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java @@ -40,8 +40,10 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.DecelerateInterpolator; import android.view.animation.OvershootInterpolator; +import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; @@ -109,6 +111,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N void needEnterComment(); void doOnIdle(Runnable runnable); + default void openAvatarsSearch() { + + } } public float translationProgress; @@ -295,6 +300,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } protected BaseFragment baseFragment; + protected boolean inBubbleMode; private ActionBarPopupWindow sendPopupWindow; private ActionBarPopupWindow.ActionBarPopupWindowLayout sendPopupLayout; private ActionBarMenuSubItem[] itemCells; @@ -324,6 +330,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private AnimatorSet commentsAnimator; + protected int avatarPicker; + protected boolean avatarSearch; + private int selectedId; protected float cornerRadius = 1.0f; @@ -336,6 +345,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N protected ActionBarMenuItem searchItem; protected ActionBarMenuItem doneItem; protected TextView selectedTextView; + private float baseSelectedTextViewTranslationY; private boolean menuShowed; protected SizeNotifierFrameLayout sizeNotifierFrameLayout; @@ -543,6 +553,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N public ChatAttachAlert(Context context, final BaseFragment parentFragment) { super(context, false); drawNavigationBar = true; + inBubbleMode = parentFragment instanceof ChatActivity && parentFragment.isInBubbleMode(); openInterpolator = new OvershootInterpolator(0.7f); baseFragment = parentFragment; setDelegate(this); @@ -579,7 +590,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int totalHeight = MeasureSpec.getSize(heightMeasureSpec); - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= 21 && !inBubbleMode) { ignoreLayout = true; setPadding(backgroundPaddingLeft, AndroidUtilities.statusBarHeight, backgroundPaddingLeft, 0); ignoreLayout = false; @@ -645,7 +656,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N continue; } if (commentTextView != null && commentTextView.isPopupView(child)) { - if (AndroidUtilities.isInMultiwindow || AndroidUtilities.isTablet()) { + if (inBubbleMode) { + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize + getPaddingTop(), MeasureSpec.EXACTLY)); + } else if (AndroidUtilities.isInMultiwindow || AndroidUtilities.isTablet()) { if (AndroidUtilities.isTablet()) { child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(Math.min(AndroidUtilities.dp(AndroidUtilities.isTablet() ? 200 : 320), heightSize - AndroidUtilities.statusBarHeight + getPaddingTop()), MeasureSpec.EXACTLY)); } else { @@ -790,7 +803,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N rad = 1.0f - moveProgress; } - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= 21 && !inBubbleMode) { top += AndroidUtilities.statusBarHeight; y += AndroidUtilities.statusBarHeight; height -= AndroidUtilities.statusBarHeight; @@ -845,6 +858,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @Override protected void onDraw(Canvas canvas) { + if (inBubbleMode) { + return; + } int color1 = Theme.getColor(Theme.key_dialogBackground); int finalColor = Color.argb((int) (255 * actionBar.getAlpha()), (int) (Color.red(color1) * 0.8f), (int) (Color.green(color1) * 0.8f), (int) (Color.blue(color1) * 0.8f)); Theme.dialogs_onlineCirclePaint.setColor(finalColor); @@ -859,6 +875,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N if (currentSheetAnimationType == 1) { if (translationY < 0) { currentAttachLayout.setTranslationY(translationY); + if (avatarPicker != 0) { + selectedTextView.setTranslationY(baseSelectedTextViewTranslationY + translationY); + } translationY = 0; buttonsRecyclerView.setTranslationY(0); } else { @@ -950,6 +969,11 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N searchItem.setTranslationX(-AndroidUtilities.dp(42)); searchItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_dialogButtonSelector), 6)); searchItem.setOnClickListener(v -> { + if (avatarPicker != 0) { + delegate.openAvatarsSearch(); + dismiss(); + return; + } final HashMap photos = new HashMap<>(); final ArrayList order = new ArrayList<>(); PhotoPickerSearchActivity fragment = new PhotoPickerSearchActivity(photos, order, 0, true, (ChatActivity) baseFragment); @@ -1005,7 +1029,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } }); fragment.setMaxSelectedPhotos(maxSelectedPhotos, allowOrder); - parentFragment.presentFragment(fragment); + baseFragment.presentFragment(fragment); dismiss(); }); @@ -1090,7 +1114,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } openContactsLayout(); } else if (num == 6) { - if (!AndroidUtilities.isGoogleMapsInstalled(parentFragment)) { + if (!AndroidUtilities.isGoogleMapsInstalled(baseFragment)) { return; } if (locationLayout == null) { @@ -1164,12 +1188,28 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N editText.setSingleLine(true); frameLayout2.addView(commentTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 0, 84, 0)); - writeButtonContainer = new FrameLayout(context); + writeButtonContainer = new FrameLayout(context) { + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + if (currentAttachLayout == photoLayout) { + info.setText(LocaleController.formatPluralString("AccDescrSendPhotos", photoLayout.getSelectedItemsCount())); + } else if (currentAttachLayout == documentLayout) { + info.setText(LocaleController.formatPluralString("AccDescrSendFiles", documentLayout.getSelectedItemsCount())); + } else if (currentAttachLayout == audioLayout) { + info.setText(LocaleController.formatPluralString("AccDescrSendAudio", audioLayout.getSelectedItemsCount())); + } + info.setClassName(Button.class.getName()); + info.setLongClickable(true); + info.setClickable(true); + } + }; + writeButtonContainer.setFocusable(true); + writeButtonContainer.setFocusableInTouchMode(true); writeButtonContainer.setVisibility(View.INVISIBLE); writeButtonContainer.setScaleX(0.2f); writeButtonContainer.setScaleY(0.2f); writeButtonContainer.setAlpha(0.0f); - writeButtonContainer.setContentDescription(LocaleController.getString("Send", R.string.Send)); containerView.addView(writeButtonContainer, LayoutHelper.createFrame(60, 60, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 6, 10)); writeButton = new ImageView(context); @@ -1184,6 +1224,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N writeButton.setBackgroundDrawable(writeButtonDrawable); writeButton.setImageResource(R.drawable.attach_send); writeButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingIcon), PorterDuff.Mode.SRC_IN)); + writeButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); writeButton.setScaleType(ImageView.ScaleType.CENTER); if (Build.VERSION.SDK_INT >= 21) { writeButton.setOutlineProvider(new ViewOutlineProvider() { @@ -1196,8 +1237,8 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } writeButtonContainer.addView(writeButton, LayoutHelper.createFrame(Build.VERSION.SDK_INT >= 21 ? 56 : 60, Build.VERSION.SDK_INT >= 21 ? 56 : 60, Gravity.LEFT | Gravity.TOP, Build.VERSION.SDK_INT >= 21 ? 2 : 0, 0, 0, 0)); writeButton.setOnClickListener(v -> { - if (editingMessageObject == null && parentFragment instanceof ChatActivity && ((ChatActivity) parentFragment).isInScheduleMode()) { - AlertsCreator.createScheduleDatePickerDialog(getContext(), ((ChatActivity) parentFragment).getDialogId(), (notify, scheduleDate) -> { + if (editingMessageObject == null && baseFragment instanceof ChatActivity && ((ChatActivity) baseFragment).isInScheduleMode()) { + AlertsCreator.createScheduleDatePickerDialog(getContext(), ((ChatActivity) baseFragment).getDialogId(), (notify, scheduleDate) -> { if (currentAttachLayout == photoLayout) { sendPressed(notify, scheduleDate); } else { @@ -1276,10 +1317,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } itemCells[a].setMinimumWidth(AndroidUtilities.dp(196)); - sendPopupLayout.addView(itemCells[a], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * i, 0, 0)); + sendPopupLayout.addView(itemCells[a], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); int chatId = chat == null ? -1 : chat.id; - itemCells[a].setOnClickListener(v -> { if (sendPopupWindow != null && sendPopupWindow.isShowing()) { sendPopupWindow.dismiss(); @@ -1321,6 +1361,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N }); i++; } + sendPopupLayout.setupRadialSelectors(Theme.getColor(Theme.key_dialogButtonSelector)); sendPopupWindow = new ActionBarPopupWindow(sendPopupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT); sendPopupWindow.setAnimationEnabled(false); @@ -1967,13 +2008,13 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } float offset = actionBar.getAlpha() != 0 ? 0.0f : AndroidUtilities.dp(26 * (1.0f - selectedTextView.getAlpha())); - if (menuShowed) { + if (menuShowed && avatarPicker == 0) { selectedMenuItem.setTranslationY(scrollOffsetY[idx] - AndroidUtilities.dp(37 + finalMove * moveProgress) + offset); } else { selectedMenuItem.setTranslationY(ActionBar.getCurrentActionBarHeight() - AndroidUtilities.dp(4) - AndroidUtilities.dp(37 + finalMove)); } searchItem.setTranslationY(ActionBar.getCurrentActionBarHeight() - AndroidUtilities.dp(4) - AndroidUtilities.dp(37 + finalMove)); - selectedTextView.setTranslationY(scrollOffsetY[idx] - AndroidUtilities.dp(25 + finalMove * moveProgress) + offset); + selectedTextView.setTranslationY(baseSelectedTextViewTranslationY = scrollOffsetY[idx] - AndroidUtilities.dp(25 + finalMove * moveProgress) + offset); if (pollLayout != null && layout == pollLayout) { if (AndroidUtilities.isTablet()) { finalMove = 63; @@ -2005,15 +2046,15 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N actionBarAnimation.cancel(); actionBarAnimation = null; } - boolean needsSearchItem = currentAttachLayout == photoLayout && !menuShowed && baseFragment instanceof ChatActivity && ((ChatActivity) baseFragment).allowSendGifs(); + boolean needsSearchItem = avatarSearch || currentAttachLayout == photoLayout && !menuShowed && baseFragment instanceof ChatActivity && ((ChatActivity) baseFragment).allowSendGifs(); if (show) { if (needsSearchItem) { searchItem.setVisibility(View.VISIBLE); } - if (!menuShowed && currentAttachLayout == photoLayout) { + if (avatarPicker != 0 || !menuShowed && currentAttachLayout == photoLayout) { selectedMenuItem.setVisibility(View.VISIBLE); } - } else { + } else if (avatarPicker == 0) { buttonsRecyclerView.setVisibility(View.VISIBLE); } if (animated) { @@ -2025,7 +2066,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N if (needsSearchItem) { animators.add(ObjectAnimator.ofFloat(searchItem, View.ALPHA, show ? 1.0f : 0.0f)); } - if (!menuShowed && currentAttachLayout == photoLayout) { + if (avatarPicker != 0 || !menuShowed && currentAttachLayout == photoLayout) { animators.add(ObjectAnimator.ofFloat(selectedMenuItem, View.ALPHA, show ? 1.0f : 0.0f)); } actionBarAnimation.playTogether(animators); @@ -2034,10 +2075,12 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N public void onAnimationEnd(Animator animation) { if (actionBarAnimation != null) { if (show) { - buttonsRecyclerView.setVisibility(View.INVISIBLE); + if (avatarPicker == 0) { + buttonsRecyclerView.setVisibility(View.INVISIBLE); + } } else { searchItem.setVisibility(View.INVISIBLE); - if (!menuShowed) { + if (avatarPicker != 0 || !menuShowed) { selectedMenuItem.setVisibility(View.INVISIBLE); } } @@ -2052,19 +2095,21 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N actionBarAnimation.start(); } else { if (show) { - buttonsRecyclerView.setVisibility(View.INVISIBLE); + if (avatarPicker == 0) { + buttonsRecyclerView.setVisibility(View.INVISIBLE); + } } actionBar.setAlpha(show ? 1.0f : 0.0f); actionBarShadow.setAlpha(show ? 1.0f : 0.0f); if (needsSearchItem) { searchItem.setAlpha(show ? 1.0f : 0.0f); } - if (!menuShowed && currentAttachLayout == photoLayout) { + if (avatarPicker != 0 || !menuShowed && currentAttachLayout == photoLayout) { selectedMenuItem.setAlpha(show ? 1.0f : 0.0f); } if (!show) { searchItem.setVisibility(View.INVISIBLE); - if (!menuShowed) { + if (avatarPicker != 0 || !menuShowed) { selectedMenuItem.setVisibility(View.INVISIBLE); } } @@ -2114,15 +2159,17 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } currentAttachLayout.onSelectedItemsCountChanged(count); - if (currentAttachLayout == photoLayout && (baseFragment instanceof ChatActivity) && (count == 0 && menuShowed || count != 0 && !menuShowed)) { - menuShowed = count != 0; + if (currentAttachLayout == photoLayout && ((baseFragment instanceof ChatActivity) || avatarPicker != 0) && (count == 0 && menuShowed || (count != 0 || avatarPicker != 0) && !menuShowed)) { + menuShowed = count != 0 || avatarPicker != 0; if (menuAnimator != null) { menuAnimator.cancel(); menuAnimator = null; } boolean needsSearchItem = actionBar.getTag() != null && baseFragment instanceof ChatActivity && ((ChatActivity) baseFragment).allowSendGifs(); if (menuShowed) { - selectedMenuItem.setVisibility(View.VISIBLE); + if (avatarPicker == 0) { + selectedMenuItem.setVisibility(View.VISIBLE); + } selectedTextView.setVisibility(View.VISIBLE); } else { if (actionBar.getTag() != null) { @@ -2130,7 +2177,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } } if (animated == 0) { - if (actionBar.getTag() == null) { + if (actionBar.getTag() == null && avatarPicker == 0) { selectedMenuItem.setAlpha(menuShowed ? 1.0f : 0.0f); } selectedTextView.setAlpha(menuShowed ? 1.0f : 0.0f); @@ -2143,7 +2190,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } else { menuAnimator = new AnimatorSet(); ArrayList animators = new ArrayList<>(); - if (actionBar.getTag() == null) { + if (actionBar.getTag() == null && avatarPicker == 0) { animators.add(ObjectAnimator.ofFloat(selectedMenuItem, View.ALPHA, menuShowed ? 1.0f : 0.0f)); } animators.add(ObjectAnimator.ofFloat(selectedTextView, View.ALPHA, menuShowed ? 1.0f : 0.0f)); @@ -2156,7 +2203,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N public void onAnimationEnd(Animator animation) { menuAnimator = null; if (!menuShowed) { - if (actionBar.getTag() == null) { + if (actionBar.getTag() == null && avatarPicker == 0) { selectedMenuItem.setVisibility(View.INVISIBLE); } selectedTextView.setVisibility(View.INVISIBLE); @@ -2267,6 +2314,20 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N currentAttachLayout.onContainerTranslationUpdated(); } + public void setAvatarPicker(int type, boolean search) { + avatarPicker = type; + avatarSearch = search; + if (avatarPicker != 0) { + buttonsRecyclerView.setVisibility(View.GONE); + shadow.setVisibility(View.GONE); + if (avatarPicker == 2) { + selectedTextView.setText(LocaleController.getString("ChoosePhotoOrVideo", R.string.ChoosePhotoOrVideo)); + } else { + selectedTextView.setText(LocaleController.getString("ChoosePhoto", R.string.ChoosePhoto)); + } + } + } + public void setMaxSelectedPhotos(int value, boolean order) { if (editingMessageObject != null) { return; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertAudioLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertAudioLayout.java index 2e35ec14b..9f23246c3 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertAudioLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertAudioLayout.java @@ -166,7 +166,7 @@ public class ChatAttachAlertAudioLayout extends ChatAttachAlert.AttachAlertLayou listView = new RecyclerListView(context) { @Override protected boolean allowSelectChildAtPosition(float x, float y) { - return y >= parentAlert.scrollOffsetY[0] + AndroidUtilities.dp(30) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + return y >= parentAlert.scrollOffsetY[0] + AndroidUtilities.dp(30) + (Build.VERSION.SDK_INT >= 21 && !parentAlert.inBubbleMode ? AndroidUtilities.statusBarHeight : 0); } }; listView.setClipToPadding(false); @@ -604,7 +604,7 @@ public class ChatAttachAlertAudioLayout extends ChatAttachAlert.AttachAlertLayou playingAudio = messageObject; ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); - return MediaController.getInstance().setPlaylist(arrayList, messageObject); + return MediaController.getInstance().setPlaylist(arrayList, messageObject, 0); } }; sharedAudioCell.setCheckForButtonPress(true); @@ -771,7 +771,7 @@ public class ChatAttachAlertAudioLayout extends ChatAttachAlert.AttachAlertLayou playingAudio = messageObject; ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); - return MediaController.getInstance().setPlaylist(arrayList, messageObject); + return MediaController.getInstance().setPlaylist(arrayList, messageObject, 0); } }; sharedAudioCell.setCheckForButtonPress(true); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertContactsLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertContactsLayout.java index 3507085ef..f18d8e65a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertContactsLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertContactsLayout.java @@ -283,7 +283,7 @@ public class ChatAttachAlertContactsLayout extends ChatAttachAlert.AttachAlertLa listView = new RecyclerListView(context) { @Override protected boolean allowSelectChildAtPosition(float x, float y) { - return y >= parentAlert.scrollOffsetY[0] + AndroidUtilities.dp(30) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + return y >= parentAlert.scrollOffsetY[0] + AndroidUtilities.dp(30) + (Build.VERSION.SDK_INT >= 21 && !parentAlert.inBubbleMode ? AndroidUtilities.statusBarHeight : 0); } }; listView.setClipToPadding(false); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java index b00a85c39..0a5f90fcf 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertDocumentLayout.java @@ -32,6 +32,7 @@ import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.BuildVars; +import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; @@ -100,7 +101,6 @@ public class ChatAttachAlertDocumentLayout extends ChatAttachAlert.AttachAlertLa private ArrayList items = new ArrayList<>(); private boolean receiverRegistered = false; private ArrayList history = new ArrayList<>(); - private static final long sizeLimit = 1024 * 1024 * 1536; private DocumentSelectActivityDelegate delegate; private HashMap selectedFiles = new HashMap<>(); private boolean scrolling; @@ -542,11 +542,9 @@ public class ChatAttachAlertDocumentLayout extends ChatAttachAlert.AttachAlertLa showErrorBox(LocaleController.formatString("PassportUploadNotImage", R.string.PassportUploadNotImage)); return false; } - if (sizeLimit != 0) { - if (item.file.length() > sizeLimit) { - showErrorBox(LocaleController.formatString("FileUploadLimit", R.string.FileUploadLimit, AndroidUtilities.formatFileSize(sizeLimit))); - return false; - } + if (item.file.length() > FileLoader.MAX_FILE_SIZE) { + showErrorBox(LocaleController.formatString("FileUploadLimit", R.string.FileUploadLimit, AndroidUtilities.formatFileSize(FileLoader.MAX_FILE_SIZE))); + return false; } if (maxSelectedFiles >= 0 && selectedFiles.size() >= maxSelectedFiles) { showErrorBox(LocaleController.formatString("PassportUploadMaxReached", R.string.PassportUploadMaxReached, LocaleController.formatPluralString("Files", maxSelectedFiles))); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java index 6e8f278d0..c3bf4c01c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java @@ -1,3 +1,11 @@ +/* + * This is the source code of Telegram for Android v. 6.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-2020. + */ + package org.telegram.ui.Components; import android.Manifest; @@ -543,7 +551,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou return; } if (Build.VERSION.SDK_INT >= 23) { - if (position == 0 && noCameraPermissions) { + if (adapter.needCamera && selectedAlbumEntry == galleryAlbumEntry && position == 0 && noCameraPermissions) { try { parentAlert.baseFragment.getParentActivity().requestPermissions(new String[]{Manifest.permission.CAMERA}, 18); } catch (Exception ignore) { @@ -577,7 +585,11 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou type = 0; } else { chatActivity = null; - type = 4; + if (parentAlert.avatarPicker != 0) { + type = PhotoViewer.SELECT_TYPE_AVATAR; + } else { + type = 4; + } } PhotoViewer.getInstance().openPhotoForSelect(arrayList, position, type, false, photoViewerProvider, chatActivity); AndroidUtilities.hideKeyboard(parentAlert.baseFragment.getFragmentView().findFocus()); @@ -740,7 +752,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou @Override public boolean shutterLongPressed() { - if (!(parentAlert.baseFragment instanceof ChatActivity) || takingPhoto || parentAlert.baseFragment == null || parentAlert.baseFragment.getParentActivity() == null || cameraView == null) { + if (parentAlert.avatarPicker != 2 && !(parentAlert.baseFragment instanceof ChatActivity) || takingPhoto || parentAlert.baseFragment == null || parentAlert.baseFragment.getParentActivity() == null || cameraView == null) { return false; } if (Build.VERSION.SDK_INT >= 23) { @@ -768,14 +780,20 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou AndroidUtilities.runOnUIThread(videoRecordRunnable, 1000); }; AndroidUtilities.lockOrientation(parentAlert.baseFragment.getParentActivity()); - CameraController.getInstance().recordVideo(cameraView.getCameraSession(), outputFile, (thumbPath, duration) -> { - if (outputFile == null || parentAlert.baseFragment == null) { + CameraController.getInstance().recordVideo(cameraView.getCameraSession(), outputFile, parentAlert.avatarPicker != 0, (thumbPath, duration) -> { + if (outputFile == null || parentAlert.baseFragment == null || cameraView == null) { return; } mediaFromExternalCamera = false; MediaController.PhotoEntry photoEntry = new MediaController.PhotoEntry(0, lastImageId--, 0, outputFile.getAbsolutePath(), 0, true, 0, 0, 0); photoEntry.duration = (int) duration; photoEntry.thumbPath = thumbPath; + if (parentAlert.avatarPicker != 0 && cameraView.isFrontface()) { + photoEntry.cropState = new MediaController.CropState(); + photoEntry.cropState.mirrored = true; + photoEntry.cropState.freeform = false; + photoEntry.cropState.lockedAspectRatio = 1.0f; + } openPhotoViewer(photoEntry, false, false); }, () -> AndroidUtilities.runOnUIThread(videoRecordRunnable, 1000)); shutterButton.setState(ShutterButton.State.RECORDING, true); @@ -805,7 +823,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou } final File cameraFile = AndroidUtilities.generatePicturePath(parentAlert.baseFragment instanceof ChatActivity && ((ChatActivity) parentAlert.baseFragment).isSecretChat(), null); final boolean sameTakePictureOrientation = cameraView.getCameraSession().isSameTakePictureOrientation(); - cameraView.getCameraSession().setFlipFront(parentAlert.baseFragment instanceof ChatActivity); + cameraView.getCameraSession().setFlipFront(parentAlert.baseFragment instanceof ChatActivity || parentAlert.avatarPicker == 2); takingPhoto = CameraController.getInstance().takePicture(cameraFile, cameraView.getCameraSession(), () -> { takingPhoto = false; if (cameraFile == null || parentAlert.baseFragment == null) { @@ -1017,7 +1035,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou dropDownContainer.removeAllSubItems(); if (mediaEnabled) { ArrayList albums; - if (parentAlert.baseFragment instanceof ChatActivity) { + if (parentAlert.baseFragment instanceof ChatActivity || parentAlert.avatarPicker == 2) { albums = MediaController.allMediaAlbums; } else { albums = MediaController.allPhotoAlbums; @@ -1217,9 +1235,23 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou type = 2; } else { chatActivity = null; - type = 5; + if (parentAlert.avatarPicker != 0) { + type = PhotoViewer.SELECT_TYPE_AVATAR; + } else { + type = 5; + } } - PhotoViewer.getInstance().openPhotoForSelect(getAllPhotosArray(), cameraPhotos.size() - 1, type, false, new BasePhotoProvider() { + ArrayList arrayList; + int index; + if (parentAlert.avatarPicker != 0) { + arrayList = new ArrayList<>(); + arrayList.add(entry); + index = 0; + } else { + arrayList = getAllPhotosArray(); + index = cameraPhotos.size() - 1; + } + PhotoViewer.getInstance().openPhotoForSelect(arrayList, index, type, false, new BasePhotoProvider() { @Override public ImageReceiver.BitmapHolder getThumbForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { @@ -1373,7 +1405,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou } private void updatePhotosCounter(boolean added) { - if (counterTextView == null) { + if (counterTextView == null || parentAlert.avatarPicker != 0) { return; } boolean hasVideo = false; @@ -1484,6 +1516,11 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou if (cameraView == null || cameraInitAnimation != null || !cameraView.isInitied()) { return; } + if (parentAlert.avatarPicker == 2 || parentAlert.baseFragment instanceof ChatActivity) { + tooltipTextView.setVisibility(VISIBLE); + } else { + tooltipTextView.setVisibility(GONE); + } if (cameraPhotos.isEmpty()) { counterTextView.setVisibility(View.INVISIBLE); cameraPhotoRecyclerView.setVisibility(View.GONE); @@ -1555,7 +1592,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou public void loadGalleryPhotos() { MediaController.AlbumEntry albumEntry; - if (parentAlert.baseFragment instanceof ChatActivity) { + if (parentAlert.baseFragment instanceof ChatActivity || parentAlert.avatarPicker == 2) { albumEntry = MediaController.allMediaAlbumEntry; } else { albumEntry = MediaController.allPhotosAlbumEntry; @@ -1947,15 +1984,8 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou float startWidth = animateCameraValues[1]; float startHeight = animateCameraValues[2]; boolean isPortrait = AndroidUtilities.displaySize.x < AndroidUtilities.displaySize.y; - float endWidth; - float endHeight; - if (isPortrait) { - endWidth = parentAlert.getContainer().getWidth() - parentAlert.getLeftInset() - parentAlert.getRightInset(); - endHeight = parentAlert.getContainer().getHeight() - parentAlert.getBottomInset(); - } else { - endWidth = parentAlert.getContainer().getWidth() - parentAlert.getLeftInset() - parentAlert.getRightInset(); - endHeight = parentAlert.getContainer().getHeight() - parentAlert.getBottomInset(); - } + float endWidth = parentAlert.getContainer().getWidth() - parentAlert.getLeftInset() - parentAlert.getRightInset(); + float endHeight = parentAlert.getContainer().getHeight() - parentAlert.getBottomInset(); if (value == 0) { cameraView.setClipTop(cameraViewOffsetY); cameraView.setClipBottom(cameraViewOffsetBottomY); @@ -2038,7 +2068,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou } else { cameraViewOffsetX = 0; } - int maxY = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); + int maxY = (Build.VERSION.SDK_INT >= 21 && !parentAlert.inBubbleMode ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); if (cameraViewLocation[1] < maxY) { cameraViewOffsetY = maxY - cameraViewLocation[1]; if (cameraViewOffsetY >= itemSize) { @@ -2185,23 +2215,32 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou } } else if (id == open_in) { try { - if (parentAlert.baseFragment instanceof ChatActivity) { + if (parentAlert.baseFragment instanceof ChatActivity || parentAlert.avatarPicker == 2) { Intent videoPickerIntent = new Intent(); videoPickerIntent.setType("video/*"); videoPickerIntent.setAction(Intent.ACTION_GET_CONTENT); - videoPickerIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, (long) (1024 * 1024 * 1536)); + videoPickerIntent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, FileLoader.MAX_FILE_SIZE); Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType("image/*"); Intent chooserIntent = Intent.createChooser(photoPickerIntent, null); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{videoPickerIntent}); - parentAlert.baseFragment.getParentActivity().startActivityForResult(chooserIntent, 1); + if (parentAlert.avatarPicker != 0) { + parentAlert.baseFragment.startActivityForResult(chooserIntent, 14); + } else { + parentAlert.baseFragment.startActivityForResult(chooserIntent, 1); + } } else { Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType("image/*"); - parentAlert.baseFragment.startActivityForResult(photoPickerIntent, 1); + if (parentAlert.avatarPicker != 0) { + parentAlert.baseFragment.startActivityForResult(photoPickerIntent, 14); + } else { + parentAlert.baseFragment.startActivityForResult(photoPickerIntent, 1); + } } + parentAlert.dismiss(); } catch (Exception e) { FileLog.e(e); } @@ -2351,11 +2390,11 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou cameraIcon.setEnabled(mediaEnabled); } if (parentAlert.baseFragment instanceof ChatActivity) { - TLRPC.Chat chat = ((ChatActivity) parentAlert.baseFragment).getCurrentChat(); galleryAlbumEntry = MediaController.allMediaAlbumEntry; if (mediaEnabled) { progressView.setText(LocaleController.getString("NoPhotos", R.string.NoPhotos)); } else { + TLRPC.Chat chat = ((ChatActivity) parentAlert.baseFragment).getCurrentChat(); if (ChatObject.isActionBannedByDefault(chat, ChatObject.ACTION_SEND_MEDIA)) { progressView.setText(LocaleController.getString("GlobalAttachMediaRestricted", R.string.GlobalAttachMediaRestricted)); } else if (AndroidUtilities.isBannedForever(chat.banned_rights)) { @@ -2365,7 +2404,11 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou } } } else { - galleryAlbumEntry = MediaController.allPhotosAlbumEntry; + if (parentAlert.avatarPicker == 2) { + galleryAlbumEntry = MediaController.allMediaAlbumEntry; + } else { + galleryAlbumEntry = MediaController.allPhotosAlbumEntry; + } } if (Build.VERSION.SDK_INT >= 23) { noGalleryPermissions = parentAlert.baseFragment.getParentActivity().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED; @@ -2749,7 +2792,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou public void didReceivedNotification(int id, int account, Object... args) { if (id == NotificationCenter.albumsDidLoad) { if (adapter != null) { - if (parentAlert.baseFragment instanceof ChatActivity) { + if (parentAlert.baseFragment instanceof ChatActivity || parentAlert.avatarPicker == 2) { galleryAlbumEntry = MediaController.allMediaAlbumEntry; } else { galleryAlbumEntry = MediaController.allPhotosAlbumEntry; @@ -2828,7 +2871,7 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou cell.setClipToOutline(true); } cell.setDelegate(v -> { - if (!mediaEnabled) { + if (!mediaEnabled || parentAlert.avatarPicker != 0) { return; } int index = (Integer) v.getTag(); @@ -2890,6 +2933,9 @@ public class ChatAttachAlertPhotoLayout extends ChatAttachAlert.AttachAlertLayou } else { cell.setIsVertical(cameraPhotoLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL); } + if (parentAlert.avatarPicker != 0) { + cell.getCheckBox().setVisibility(GONE); + } MediaController.PhotoEntry photoEntry = getPhotoEntryAtPosition(position); cell.setPhotoEntry(photoEntry, needCamera && selectedAlbumEntry == galleryAlbumEntry, position == getItemCount() - 1); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatGreetingsView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatGreetingsView.java new file mode 100644 index 000000000..afd864fd9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatGreetingsView.java @@ -0,0 +1,172 @@ +package org.telegram.ui.Components; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.Emoji; +import org.telegram.messenger.FileLoader; +import org.telegram.messenger.ImageLocation; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessageObject; +import org.telegram.messenger.R; +import org.telegram.messenger.UserConfig; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.ActionBar.Theme; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.Random; + +public class ChatGreetingsView extends LinearLayout { + + private TextView titleView; + private TextView descriptionView; + private Listener listener; + + public BackupImageView stickerToSendView; + + public ChatGreetingsView(Context context, TLRPC.User user, int distance, TLRPC.Document preloadedGreetingsSticker) { + super(context); + setOrientation(VERTICAL); + + titleView = new TextView(context); + titleView.setTextSize(14); + titleView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + titleView.setGravity(Gravity.CENTER_HORIZONTAL); + + descriptionView = new TextView(context); + descriptionView.setTextSize(14); + descriptionView.setGravity(Gravity.CENTER_HORIZONTAL); + + + stickerToSendView = new BackupImageView(context); + + addView(titleView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 20, 14, 20, 14)); + addView(descriptionView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 20, 12, 20, 0)); + addView(stickerToSendView, LayoutHelper.createLinear(112, 112, Gravity.CENTER_HORIZONTAL, 0, 16, 0, 16)); + + updateColors(); + + titleView.setText(LocaleController.formatString("NearbyPeopleGreetingsMessage", R.string.NearbyPeopleGreetingsMessage, user.first_name, LocaleController.formatDistance(distance, 1))); + descriptionView.setText(LocaleController.getString("NearbyPeopleGreetingsDescription", R.string.NearbyPeopleGreetingsDescription)); + + if (preloadedGreetingsSticker == null) { + TLRPC.TL_messages_getStickers req = new TLRPC.TL_messages_getStickers(); + req.emoticon = "\uD83D\uDC4B" + Emoji.fixEmoji("⭐"); + ConnectionsManager.getInstance(UserConfig.selectedAccount).sendRequest(req, (response, error) -> { + if (response instanceof TLRPC.TL_messages_stickers) { + ArrayList list = ((TLRPC.TL_messages_stickers) response).stickers; + if (!list.isEmpty()) { + TLRPC.Document sticker = list.get(Math.abs(new Random().nextInt() % list.size())); + AndroidUtilities.runOnUIThread(() -> { + setSticker(sticker); + }); + } + } + }); + + } else { + setSticker(preloadedGreetingsSticker); + } + } + + private void setSticker(TLRPC.Document sticker) { + TLRPC.PhotoSize thumb = FileLoader.getClosestPhotoSizeWithSize(sticker.thumbs, 90); + + + stickerToSendView.setImage( + ImageLocation.getForDocument(sticker), createFilter(sticker), + ImageLocation.getForDocument(thumb, sticker), null, + 0, sticker + ); + stickerToSendView.setOnClickListener(v -> { + if (listener != null) { + listener.onGreetings(sticker); + } + }); + } + + public static String createFilter(TLRPC.Document document) { + float maxHeight; + float maxWidth; + int photoWidth = 0; + int photoHeight = 0; + if (AndroidUtilities.isTablet()) { + maxHeight = maxWidth = AndroidUtilities.getMinTabletSide() * 0.4f; + } else { + maxHeight = maxWidth = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y) * 0.5f; + } + for (int a = 0; a < document.attributes.size(); a++) { + TLRPC.DocumentAttribute attribute = document.attributes.get(a); + if (attribute instanceof TLRPC.TL_documentAttributeImageSize) { + photoWidth = attribute.w; + photoHeight = attribute.h; + break; + } + } + if (MessageObject.isAnimatedStickerDocument(document, true) && photoWidth == 0 && photoHeight == 0) { + photoWidth = photoHeight = 512; + } + + if (photoWidth == 0) { + photoHeight = (int) maxHeight; + photoWidth = photoHeight + AndroidUtilities.dp(100); + } + photoHeight *= maxWidth / photoWidth; + photoWidth = (int) maxWidth; + if (photoHeight > maxHeight) { + photoWidth *= maxHeight / photoHeight; + photoHeight = (int) maxHeight; + } + + int w = (int) (photoWidth / AndroidUtilities.density); + int h = (int) (photoHeight / AndroidUtilities.density); + return String.format(Locale.US, "%d_%d", w, h); + } + + private void updateColors() { + titleView.setTextColor(Theme.getColor(Theme.key_chat_serviceText)); + descriptionView.setTextColor(Theme.getColor(Theme.key_chat_serviceText)); + setBackground(Theme.createRoundRectDrawable(AndroidUtilities.dp(10), Theme.getColor(Theme.key_chat_serviceBackground))); + } + + public void setListener(Listener listener) { + this.listener = listener; + } + + public interface Listener { + void onGreetings(TLRPC.Document sticker); + } + + boolean ignoreLayot; + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + ignoreLayot = true; + descriptionView.setVisibility(View.VISIBLE); + stickerToSendView.setVisibility(View.VISIBLE); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (getMeasuredHeight() > MeasureSpec.getSize(heightMeasureSpec)) { + descriptionView.setVisibility(View.GONE); + stickerToSendView.setVisibility(View.GONE); + } else { + descriptionView.setVisibility(View.VISIBLE); + stickerToSendView.setVisibility(View.VISIBLE); + } + ignoreLayot = false; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public void requestLayout() { + if (ignoreLayot) { + return; + } + super.requestLayout(); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox2.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox2.java index fee9df963..142f5220f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox2.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox2.java @@ -3,6 +3,8 @@ package org.telegram.ui.Components; import android.content.Context; import android.graphics.Canvas; import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.CheckBox; public class CheckBox2 extends View { @@ -78,4 +80,12 @@ public class CheckBox2 extends View { protected void onDraw(Canvas canvas) { checkBoxBase.draw(canvas); } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(CheckBox.class.getName()); + info.setChecked(isChecked()); + info.setCheckable(true); + } } 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 b67421b88..9965945ee 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java @@ -24,6 +24,8 @@ import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.FileLog; import org.telegram.messenger.ImageReceiver; +import java.util.Arrays; + public class ClippingImageView extends View { private int clipBottom; @@ -52,6 +54,7 @@ public class ClippingImageView extends View { private float[][] animationValues; private float additionalTranslationY; + private float additionalTranslationX; public ClippingImageView(Context context) { super(context); @@ -74,6 +77,10 @@ public class ClippingImageView extends View { additionalTranslationY = value; } + public void setAdditionalTranslationX(float value) { + additionalTranslationX = value; + } + @Override public void setTranslationY(float translationY) { super.setTranslationY(translationY + additionalTranslationY); @@ -95,7 +102,7 @@ public class ClippingImageView extends View { 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); + setTranslationX(animationValues[0][2] + additionalTranslationX + (animationValues[1][2] + additionalTranslationX - animationValues[0][2] - additionalTranslationX) * 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)); @@ -267,9 +274,7 @@ public class ClippingImageView extends View { public void setRadius(int[] value) { if (value == null) { needRadius = false; - for (int a = 0; a < radius.length; a++) { - radius[a] = 0; - } + Arrays.fill(radius, 0); return; } System.arraycopy(value, 0, radius, 0, value.length); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropAreaView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropAreaView.java index 823c4c53d..5deadc000 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropAreaView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropAreaView.java @@ -14,6 +14,8 @@ import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.os.Build; import androidx.annotation.Keep; + +import android.os.SystemClock; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -21,6 +23,7 @@ import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.BubbleActivity; public class CropAreaView extends View { @@ -30,9 +33,7 @@ public class CropAreaView extends View { interface AreaViewListener { void onAreaChangeBegan(); - void onAreaChange(); - void onAreaChangeEnded(); } @@ -57,17 +58,23 @@ public class CropAreaView extends View { private boolean dimVisibile; private boolean frameVisible; - Paint dimPaint; - Paint shadowPaint; - Paint linePaint; - Paint handlePaint; - Paint framePaint; + private float frameAlpha = 1.0f; + private long lastUpdateTime; - AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); + private Paint dimPaint; + private Paint shadowPaint; + private Paint linePaint; + private Paint handlePaint; + private Paint framePaint; + private Paint bitmapPaint; + + private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); private float sidePadding; private float minWidth; + private boolean inBubbleMode; + enum GridType { NONE, MINOR, MAJOR } @@ -87,9 +94,13 @@ public class CropAreaView extends View { private Animator animator; + private RectF targetRect = new RectF(); + public CropAreaView(Context context) { super(context); + inBubbleMode = context instanceof BubbleActivity; + frameVisible = true; dimVisibile = true; @@ -99,7 +110,7 @@ public class CropAreaView extends View { gridType = GridType.NONE; dimPaint = new Paint(); - dimPaint.setColor(0xcc000000); + dimPaint.setColor(0x7f000000); shadowPaint = new Paint(); shadowPaint.setStyle(Paint.Style.FILL); @@ -123,6 +134,13 @@ public class CropAreaView extends View { eraserPaint.setColor(0); eraserPaint.setStyle(Paint.Style.FILL); eraserPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + + bitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + bitmapPaint.setColor(0xffffffff); + } + + public void setIsVideo(boolean value) { + minWidth = AndroidUtilities.dp(value ? 64 : 32); } public boolean isDragging() { @@ -133,8 +151,15 @@ public class CropAreaView extends View { dimVisibile = visible; } - public void setFrameVisibility(boolean visible) { + public void setFrameVisibility(boolean visible, boolean animated) { frameVisible = visible; + if (frameVisible) { + frameAlpha = animated ? 0.0f : 1.0f; + lastUpdateTime = SystemClock.elapsedRealtime(); + invalidate(); + } else { + frameAlpha = 1.0f; + } } public void setBottomPadding(float value) { @@ -149,16 +174,13 @@ public class CropAreaView extends View { listener = l; } - public void setBitmap(Bitmap bitmap, boolean sideward, boolean fform) { - if (bitmap == null || bitmap.isRecycled()) { - return; - } + public void setBitmap(int w, int h, boolean sideward, boolean fform) { freeform = fform; float aspectRatio; if (sideward) { - aspectRatio = ((float) bitmap.getHeight()) / ((float) bitmap.getWidth()); + aspectRatio = h / (float) w; } else { - aspectRatio = ((float) bitmap.getWidth()) / ((float) bitmap.getHeight()); + aspectRatio = w / (float) h; } if (!freeform) { @@ -213,31 +235,39 @@ public class CropAreaView extends View { int gridHeight = height - handleThickness * 2; GridType type = gridType; - if (type == GridType.NONE && gridProgress > 0) + if (type == GridType.NONE && gridProgress > 0) { type = previousGridType; + } - shadowPaint.setAlpha((int) (gridProgress * 26)); - linePaint.setAlpha((int) (gridProgress * 178)); + shadowPaint.setAlpha((int) (gridProgress * 26 * frameAlpha)); + linePaint.setAlpha((int) (gridProgress * 178 * frameAlpha)); + framePaint.setAlpha((int) (178 * frameAlpha)); + handlePaint.setAlpha((int) (255 * frameAlpha)); for (int i = 0; i < 3; i++) { if (type == GridType.MINOR) { for (int j = 1; j < 4; j++) { - if (i == 2 && j == 3) + if (i == 2 && j == 3) { continue; + } - canvas.drawLine(originX + handleThickness + gridWidth / 3 / 3 * j + gridWidth / 3 * i, originY + handleThickness, originX + handleThickness + gridWidth / 3 / 3 * j + gridWidth / 3 * i, originY + handleThickness + gridHeight, shadowPaint); - canvas.drawLine(originX + handleThickness + gridWidth / 3 / 3 * j + gridWidth / 3 * i, originY + handleThickness, originX + handleThickness + gridWidth / 3 / 3 * j + gridWidth / 3 * i, originY + handleThickness + gridHeight, linePaint); + int startX = originX + handleThickness + gridWidth / 3 / 3 * j + gridWidth / 3 * i; + canvas.drawLine(startX, originY + handleThickness, startX, originY + handleThickness + gridHeight, shadowPaint); + canvas.drawLine(startX, originY + handleThickness, startX, originY + handleThickness + gridHeight, linePaint); - canvas.drawLine(originX + handleThickness, originY + handleThickness + gridHeight / 3 / 3 * j + gridHeight / 3 * i, originX + handleThickness + gridWidth, originY + handleThickness + gridHeight / 3 / 3 * j + gridHeight / 3 * i, shadowPaint); - canvas.drawLine(originX + handleThickness, originY + handleThickness + gridHeight / 3 / 3 * j + gridHeight / 3 * i, originX + handleThickness + gridWidth, originY + handleThickness + gridHeight / 3 / 3 * j + gridHeight / 3 * i, linePaint); + int startY = originY + handleThickness + gridHeight / 3 / 3 * j + gridHeight / 3 * i; + canvas.drawLine(originX + handleThickness, startY, originX + handleThickness + gridWidth, startY, shadowPaint); + canvas.drawLine(originX + handleThickness, startY, originX + handleThickness + gridWidth, startY, linePaint); } } else if (type == GridType.MAJOR) { if (i > 0) { - canvas.drawLine(originX + handleThickness + gridWidth / 3 * i, originY + handleThickness, originX + handleThickness + gridWidth / 3 * i, originY + handleThickness + gridHeight, shadowPaint); - canvas.drawLine(originX + handleThickness + gridWidth / 3 * i, originY + handleThickness, originX + handleThickness + gridWidth / 3 * i, originY + handleThickness + gridHeight, linePaint); + int startX = originX + handleThickness + gridWidth / 3 * i; + canvas.drawLine(startX, originY + handleThickness, startX, originY + handleThickness + gridHeight, shadowPaint); + canvas.drawLine(startX, originY + handleThickness, startX, originY + handleThickness + gridHeight, linePaint); - canvas.drawLine(originX + handleThickness, originY + handleThickness + gridHeight / 3 * i, originX + handleThickness + gridWidth, originY + handleThickness + gridHeight / 3 * i, shadowPaint); - canvas.drawLine(originX + handleThickness, originY + handleThickness + gridHeight / 3 * i, originX + handleThickness + gridWidth, originY + handleThickness + gridHeight / 3 * i, linePaint); + int startY = originY + handleThickness + gridHeight / 3 * i; + canvas.drawLine(originX + handleThickness, startY, originX + handleThickness + gridWidth, startY, shadowPaint); + canvas.drawLine(originX + handleThickness, startY, originX + handleThickness + gridWidth, startY, linePaint); } } } @@ -259,32 +289,61 @@ public class CropAreaView extends View { canvas.drawRect(originX + width - handleSize, originY + height - handleThickness, originX + width, originY + height, handlePaint); canvas.drawRect(originX + width - handleThickness, originY + height - handleSize, originX + width, originY + height, handlePaint); } else { - if (circleBitmap == null || circleBitmap.getWidth() != actualRect.width()) { + float width = getMeasuredWidth() - 2 * sidePadding; + float height = getMeasuredHeight() - bottomPadding - (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0) - 2 * sidePadding; + int size = (int) Math.min(width, height); + + if (circleBitmap == null || circleBitmap.getWidth() != size) { + boolean hasBitmap = circleBitmap != null; if (circleBitmap != null) { circleBitmap.recycle(); circleBitmap = null; } try { - circleBitmap = Bitmap.createBitmap((int) actualRect.width(), (int) actualRect.height(), Bitmap.Config.ARGB_8888); + circleBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas circleCanvas = new Canvas(circleBitmap); - circleCanvas.drawRect(0, 0, actualRect.width(), actualRect.height(), dimPaint); - circleCanvas.drawCircle(actualRect.width() / 2, actualRect.height() / 2, actualRect.width() / 2, eraserPaint); + circleCanvas.drawRect(0, 0, size, size, dimPaint); + circleCanvas.drawCircle(size / 2, size / 2, size / 2, eraserPaint); circleCanvas.setBitmap(null); + if (!hasBitmap) { + frameAlpha = 0.0f; + lastUpdateTime = SystemClock.elapsedRealtime(); + } } catch (Throwable ignore) { } } - canvas.drawRect(0, 0, getWidth(), (int) actualRect.top, dimPaint); - canvas.drawRect(0, (int) actualRect.top, (int) actualRect.left, (int) actualRect.bottom, dimPaint); - canvas.drawRect((int) actualRect.right, (int) actualRect.top, getWidth(), (int) actualRect.bottom, dimPaint); - canvas.drawRect(0, (int) actualRect.bottom, getWidth(), getHeight(), dimPaint); if (circleBitmap != null) { - canvas.drawBitmap(circleBitmap, (int) actualRect.left, (int) actualRect.top, null); + bitmapPaint.setAlpha((int) (255 * frameAlpha)); + dimPaint.setAlpha((int) (0x7f * frameAlpha)); + float left = sidePadding + (width - size) / 2.0f; + float top = sidePadding + (height - size) / 2.0f + (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); + float right = left + size; + float bottom = top + size; + canvas.drawRect(0, 0, getWidth(), (int) top, dimPaint); + canvas.drawRect(0, (int) top, (int) left, (int) bottom, dimPaint); + canvas.drawRect((int) right, (int) top, getWidth(), (int) bottom, dimPaint); + canvas.drawRect(0, (int) bottom, getWidth(), getHeight(), dimPaint); + canvas.drawBitmap(circleBitmap, (int) left, (int) top, bitmapPaint); } } + + if (frameAlpha < 1) { + long newTime = SystemClock.elapsedRealtime(); + long dt = newTime - lastUpdateTime; + if (dt > 17) { + dt = 17; + } + lastUpdateTime = newTime; + frameAlpha += dt / 180.0f; + if (frameAlpha > 1.0f) { + frameAlpha = 1.0f; + } + invalidate(); + } } - private void updateTouchAreas() { + public void updateTouchAreas() { int touchPadding = AndroidUtilities.dp(16); topLeftCorner.set(actualRect.left - touchPadding, actualRect.top - touchPadding, actualRect.left + touchPadding, actualRect.top + touchPadding); @@ -443,11 +502,11 @@ public class CropAreaView extends View { } public float getCropCenterX() { - return actualRect.left + ((actualRect.right - actualRect.left) / 2.0f); + return (actualRect.left + actualRect.right) / 2.0f; } public float getCropCenterY() { - return actualRect.top + ((actualRect.bottom - actualRect.top) / 2.0f); + return (actualRect.top + actualRect.bottom) / 2.0f; } public float getCropWidth() { @@ -459,13 +518,16 @@ public class CropAreaView extends View { } public RectF getTargetRectToFill() { - RectF rect = new RectF(); - calculateRect(rect, getAspectRatio()); - return rect; + return getTargetRectToFill(getAspectRatio()); + } + + public RectF getTargetRectToFill(float aspectRatio) { + calculateRect(targetRect, aspectRatio); + return targetRect; } public void calculateRect(RectF rect, float cropAspectRatio) { - float statusBarHeight = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + float statusBarHeight = (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); float left, top, right, bottom; float measuredHeight = (float) getMeasuredHeight() - bottomPadding - statusBarHeight; float aspectRatio = (float) getMeasuredWidth() / measuredHeight; @@ -480,7 +542,7 @@ public class CropAreaView extends View { top = centerY - (minSide / 2.0f); right = centerX + (minSide / 2.0f); bottom = centerY + (minSide / 2.0f); - } else if (cropAspectRatio > aspectRatio) { + } else if (cropAspectRatio - aspectRatio > 0.0001 || height * cropAspectRatio > width) { left = centerX - (width / 2.0f); top = centerY - ((width / cropAspectRatio) / 2.0f); right = centerX + (width / 2.0f); @@ -499,7 +561,7 @@ public class CropAreaView extends View { int x = (int) (event.getX() - ((ViewGroup) getParent()).getX()); int y = (int) (event.getY() - ((ViewGroup) getParent()).getY()); - float statusBarHeight = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + float statusBarHeight = (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); int action = event.getActionMasked(); @@ -535,25 +597,29 @@ public class CropAreaView extends View { isDragging = true; - if (listener != null) + if (listener != null) { listener.onAreaChangeBegan(); + } return true; } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { isDragging = false; - if (activeControl == Control.NONE) + if (activeControl == Control.NONE) { return false; + } activeControl = Control.NONE; - if (listener != null) + if (listener != null) { listener.onAreaChangeEnded(); + } return true; } else if (action == MotionEvent.ACTION_MOVE) { - if (activeControl == Control.NONE) + if (activeControl == Control.NONE) { return false; + } tempRect.set(actualRect); @@ -562,6 +628,7 @@ public class CropAreaView extends View { previousX = x; previousY = y; + boolean b = Math.abs(translationX) > Math.abs(translationY); switch (activeControl) { case TOP_LEFT: tempRect.left += translationX; @@ -571,7 +638,7 @@ public class CropAreaView extends View { float w = tempRect.width(); float h = tempRect.height(); - if (Math.abs(translationX) > Math.abs(translationY)) { + if (b) { constrainRectByWidth(tempRect, lockAspectRatio); } else { constrainRectByHeight(tempRect, lockAspectRatio); @@ -589,7 +656,7 @@ public class CropAreaView extends View { if (lockAspectRatio > 0) { float h = tempRect.height(); - if (Math.abs(translationX) > Math.abs(translationY)) { + if (b) { constrainRectByWidth(tempRect, lockAspectRatio); } else { constrainRectByHeight(tempRect, lockAspectRatio); @@ -606,7 +673,7 @@ public class CropAreaView extends View { if (lockAspectRatio > 0) { float w = tempRect.width(); - if (Math.abs(translationX) > Math.abs(translationY)) { + if (b) { constrainRectByWidth(tempRect, lockAspectRatio); } else { constrainRectByHeight(tempRect, lockAspectRatio); @@ -621,7 +688,7 @@ public class CropAreaView extends View { tempRect.bottom += translationY; if (lockAspectRatio > 0) { - if (Math.abs(translationX) > Math.abs(translationY)) { + if (b) { constrainRectByWidth(tempRect, lockAspectRatio); } else { constrainRectByHeight(tempRect, lockAspectRatio); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropGestureDetector.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropGestureDetector.java index b8632f3bd..20251a4e8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropGestureDetector.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropGestureDetector.java @@ -2,6 +2,8 @@ package org.telegram.ui.Components.Crop; import android.content.Context; import androidx.core.view.MotionEventCompat; + +import android.os.SystemClock; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.VelocityTracker; @@ -12,10 +14,10 @@ import org.telegram.messenger.AndroidUtilities; public class CropGestureDetector { private ScaleGestureDetector mDetector; private CropGestureListener mListener; - float mLastTouchX; - float mLastTouchY; - final float mTouchSlop; - final float mMinimumVelocity; + private float mLastTouchX; + private float mLastTouchY; + private final float mTouchSlop; + private final float mMinimumVelocity; private VelocityTracker mVelocityTracker; private boolean mIsDragging; @@ -23,12 +25,15 @@ public class CropGestureDetector { private int mActivePointerId; private int mActivePointerIndex; + private long touchTime; + private boolean started; public interface CropGestureListener { void onDrag(float dx, float dy); void onFling(float startX, float startY, float velocityX, float velocityY); void onScale(float scaleFactor, float focusX, float focusY); + void onTapUp(); } public CropGestureDetector(Context context) { @@ -103,9 +108,13 @@ public class CropGestureDetector { switch (ev.getAction() & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: this.mActivePointerId = ev.getPointerId(0); + touchTime = SystemClock.elapsedRealtime(); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: + if (!mIsDragging && SystemClock.elapsedRealtime() - touchTime < 800) { + mListener.onTapUp(); + } this.mActivePointerId = INVALID_POINTER_ID; break; case MotionEvent.ACTION_POINTER_UP: diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropRotationWheel.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropRotationWheel.java index 835eb0747..a145e5367 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropRotationWheel.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropRotationWheel.java @@ -27,7 +27,8 @@ public class CropRotationWheel extends FrameLayout { void onEnd(float angle); void aspectRatioPressed(); - void rotate90Pressed(); + boolean rotate90Pressed(); + boolean mirror(); } private static final int MAX_ANGLE = 45; @@ -37,6 +38,8 @@ public class CropRotationWheel extends FrameLayout { private Paint bluePaint; private ImageView aspectRatioButton; + private ImageView rotation90Button; + private ImageView mirrorButton; private TextView degreesLabel; protected float rotation; @@ -62,24 +65,42 @@ public class CropRotationWheel extends FrameLayout { bluePaint.setAlpha(255); bluePaint.setAntiAlias(true); + mirrorButton = new ImageView(context); + mirrorButton.setImageResource(R.drawable.photo_flip); + mirrorButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); + mirrorButton.setScaleType(ImageView.ScaleType.CENTER); + mirrorButton.setOnClickListener(v -> { + if (rotationListener != null) { + setMirrored(rotationListener.mirror()); + } + }); + mirrorButton.setOnLongClickListener(v -> { + aspectRatioButton.callOnClick(); + return true; + }); + mirrorButton.setContentDescription(LocaleController.getString("AccDescrMirror", R.string.AccDescrMirror)); + addView(mirrorButton, LayoutHelper.createFrame(70, 64, Gravity.LEFT | Gravity.CENTER_VERTICAL)); + aspectRatioButton = new ImageView(context); aspectRatioButton.setImageResource(R.drawable.tool_cropfix); aspectRatioButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); aspectRatioButton.setScaleType(ImageView.ScaleType.CENTER); aspectRatioButton.setOnClickListener(v -> { - if (rotationListener != null) + if (rotationListener != null) { rotationListener.aspectRatioPressed(); + } }); + aspectRatioButton.setVisibility(GONE); aspectRatioButton.setContentDescription(LocaleController.getString("AccDescrAspectRatio", R.string.AccDescrAspectRatio)); addView(aspectRatioButton, LayoutHelper.createFrame(70, 64, Gravity.LEFT | Gravity.CENTER_VERTICAL)); - ImageView rotation90Button = new ImageView(context); + rotation90Button = new ImageView(context); rotation90Button.setImageResource(R.drawable.tool_rotate); rotation90Button.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); rotation90Button.setScaleType(ImageView.ScaleType.CENTER); rotation90Button.setOnClickListener(v -> { if (rotationListener != null) { - rotationListener.rotate90Pressed(); + setRotated(rotationListener.rotate90Pressed()); } }); rotation90Button.setContentDescription(LocaleController.getString("AccDescrRotate", R.string.AccDescrRotate)); @@ -95,7 +116,15 @@ public class CropRotationWheel extends FrameLayout { } public void setFreeform(boolean freeform) { - aspectRatioButton.setVisibility(freeform ? VISIBLE : GONE); + //aspectRatioButton.setVisibility(freeform ? VISIBLE : GONE); + } + + public void setMirrored(boolean value) { + mirrorButton.setColorFilter(value ? new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.MULTIPLY) : null); + } + + public void setRotated(boolean value) { + rotation90Button.setColorFilter(value ? new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.MULTIPLY) : null); } @Override @@ -104,8 +133,12 @@ public class CropRotationWheel extends FrameLayout { super.onMeasure(MeasureSpec.makeMeasureSpec(Math.min(width, AndroidUtilities.dp(400)), MeasureSpec.EXACTLY), heightMeasureSpec); } - public void reset() { + public void reset(boolean resetMirror) { setRotation(0.0f, false); + if (resetMirror) { + setMirrored(false); + } + setRotated(false); } public void setListener(RotationWheelListener listener) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropState.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropState.java index 45be6d10d..90ac642fe 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropState.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropState.java @@ -49,12 +49,12 @@ public class CropState { public float getX() { updateValues(); - return values[matrix.MTRANS_X]; + return values[Matrix.MTRANS_X]; } public float getY() { updateValues(); - return values[matrix.MTRANS_Y]; + return values[Matrix.MTRANS_Y]; } public void scale(float s, float pivotX, float pivotY) { @@ -75,18 +75,6 @@ public class CropState { return rotation; } - public void reset(CropAreaView areaView) { - matrix.reset(); - - x = 0.0f; - y = 0.0f; - rotation = 0.0f; - minimumScale = areaView.getCropWidth() / width; - scale = minimumScale; - - matrix.postScale(scale, scale); - } - public void getConcatMatrix(Matrix toMatrix) { toMatrix.postConcat(matrix); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropTransform.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropTransform.java new file mode 100644 index 000000000..71c382192 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropTransform.java @@ -0,0 +1,100 @@ +/* + * This is the source code of Telegram for Android v. 6.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-2020. + */ + +package org.telegram.ui.Components.Crop; + +public class CropTransform { + + private boolean hasTransform; + private float cropPx; + private float cropPy; + private float cropAreaX; + private float cropAreaY; + private float cropScale; + private float cropRotation; + private boolean isMirrored; + private int cropOrientation; + private float cropPw; + private float cropPh; + private float trueCropScale; + private float minScale; + + public void setViewTransform(boolean set, float px, float py, float rotate, int orientation, float scale, float cs, float ms, float pw, float ph, float cx, float cy, boolean mirrored) { + hasTransform = set; + cropPx = px; + cropPy = py; + cropScale = scale; + cropRotation = rotate; + cropOrientation = orientation; + while (cropOrientation < 0) { + cropOrientation += 360; + } + while (cropOrientation >= 360) { + cropOrientation -= 360; + } + cropPw = pw; + cropPh = ph; + cropAreaX = cx; + cropAreaY = cy; + trueCropScale = cs; + minScale = ms; + isMirrored = mirrored; + } + + public boolean hasViewTransform() { + return hasTransform; + } + + public float getCropAreaX() { + return cropAreaX; + } + + public float getCropAreaY() { + return cropAreaY; + } + + public float getCropPx() { + return cropPx; + } + + public float getCropPy() { + return cropPy; + } + + public float getScale() { + return cropScale; + } + + public float getRotation() { + return cropRotation; + } + + public int getOrientation() { + return cropOrientation; + } + + public float getTrueCropScale() { + return trueCropScale; + } + + public float getMinScale() { + return minScale; + } + + public float getCropPw() { + return cropPw; + } + + public float getCropPh() { + return cropPh; + } + + public boolean isMirrored () { + return isMirrored; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropView.java index 86a52323f..7f900f293 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Crop/CropView.java @@ -3,57 +3,60 @@ package org.telegram.ui.Components.Crop; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; -import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.RectF; import android.os.Build; import android.view.MotionEvent; -import android.view.View; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import android.widget.ImageView; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.BuildVars; import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; -import org.telegram.messenger.ImageLoader; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; import org.telegram.messenger.R; import org.telegram.messenger.SharedConfig; import org.telegram.messenger.VideoEditedInfo; import org.telegram.ui.ActionBar.AlertDialog; +import org.telegram.ui.BubbleActivity; +import org.telegram.ui.Components.Paint.Swatch; +import org.telegram.ui.Components.Paint.Views.TextPaintView; import org.telegram.ui.Components.PaintingOverlay; +import org.telegram.ui.Components.Point; +import org.telegram.ui.Components.VideoEditTextureView; import java.io.File; import java.io.FileOutputStream; import java.util.ArrayList; -import static android.graphics.Paint.FILTER_BITMAP_FLAG; - public class CropView extends FrameLayout implements CropAreaView.AreaViewListener, CropGestureDetector.CropGestureListener { private static final float EPSILON = 0.00001f; private static final int RESULT_SIDE = 1280; private static final float MAX_SCALE = 30.0f; - private View backView; - private CropAreaView areaView; private ImageView imageView; - private Matrix presentationMatrix; private Matrix overlayMatrix; private PaintingOverlay paintingOverlay; + private VideoEditTextureView videoEditTextureView; + private CropTransform cropTransform; private RectF previousAreaRect; private RectF initialAreaRect; private float rotationStartScale; + private boolean inBubbleMode; + private CropRectangle tempRect; private Matrix tempMatrix; @@ -64,8 +67,14 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen private boolean animating; private CropGestureDetector detector; + float[] values = new float[9]; + private boolean hasAspectRatioDialog; + private boolean isVisible; + + private int bitmapRotation; + private class CropState { private float width; private float height; @@ -77,12 +86,12 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen private float baseRotation; private float orientation; private float rotation; + private boolean mirrored; private Matrix matrix; - private CropState(Bitmap bitmap, int bRotation) { - width = bitmap.getWidth(); - height = bitmap.getHeight(); - + private CropState(int w, int h, int bRotation) { + width = w; + height = h; x = 0.0f; y = 0.0f; scale = 1.0f; @@ -91,13 +100,12 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen matrix = new Matrix(); } - private void updateBitmap(Bitmap bitmap, int rotation) { - float ps = width / bitmap.getWidth(); + private void update(int w, int h, int rotation) { + float ps = width / w; scale *= ps; - width = bitmap.getWidth(); - height = bitmap.getHeight(); + width = w; + height = h; updateMinimumScale(); - float[] values = new float[9]; matrix.getValues(values); matrix.reset(); matrix.postScale(scale, scale); @@ -106,8 +114,7 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen } private boolean hasChanges() { - return Math.abs(x) > EPSILON || Math.abs(y) > EPSILON || Math.abs(scale - minimumScale) > EPSILON - || Math.abs(rotation) > EPSILON || Math.abs(orientation) > EPSILON; + return Math.abs(x) > EPSILON || Math.abs(y) > EPSILON || Math.abs(scale - minimumScale) > EPSILON || Math.abs(rotation) > EPSILON || Math.abs(orientation) > EPSILON; } private float getWidth() { @@ -140,6 +147,12 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen return y; } + private void setScale(float s, float pivotX, float pivotY) { + scale = s; + matrix.reset(); + matrix.setScale(s, s, pivotX, pivotY); + } + private void scale(float s, float pivotX, float pivotY) { scale *= s; matrix.postScale(s, s, pivotX, pivotY); @@ -162,18 +175,26 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen return rotation; } + private boolean isMirrored() { + return mirrored; + } + private float getOrientation() { return orientation + baseRotation; } - private float getOrientationOnly() { - return orientation; + private int getOrientationOnly() { + return (int) orientation; } private float getBaseRotation() { return baseRotation; } + private void mirror() { + mirrored = !mirrored; + } + private void reset(CropAreaView areaView, float orient, boolean freeform) { matrix.reset(); @@ -214,8 +235,9 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen public interface CropViewListener { void onChange(boolean reset); - + void onUpdate(); void onAspectLock(boolean enabled); + void onTapUp(); } private CropViewListener listener; @@ -223,21 +245,16 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen public CropView(Context context) { super(context); + inBubbleMode = context instanceof BubbleActivity; + previousAreaRect = new RectF(); initialAreaRect = new RectF(); - presentationMatrix = new Matrix(); overlayMatrix = new Matrix(); tempRect = new CropRectangle(); tempMatrix = new Matrix(); animating = false; - backView = new View(context); - backView.setBackgroundColor(0xff000000); - backView.setVisibility(INVISIBLE); - addView(backView); - imageView = new ImageView(context); - imageView.setDrawingCacheEnabled(true); imageView.setScaleType(ImageView.ScaleType.MATRIX); addView(imageView); @@ -249,18 +266,6 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen addView(areaView); } - @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - boolean result = super.drawChild(canvas, child, drawingTime); - if (child == imageView && paintingOverlay != null) { - canvas.save(); - canvas.setMatrix(overlayMatrix); - paintingOverlay.draw(canvas); - canvas.restore(); - } - return result; - } - public boolean isReady() { return !detector.isScaling() && !detector.isDragging() && !areaView.isDragging(); } @@ -278,73 +283,129 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen areaView.setActualRect(ratio); } - public void setBitmap(Bitmap b, int rotation, boolean fform, boolean same, PaintingOverlay overlay) { + public void setBitmap(Bitmap b, int rotation, boolean fform, boolean same, PaintingOverlay overlay, CropTransform transform, VideoEditTextureView videoView, MediaController.CropState restoreState) { freeform = fform; paintingOverlay = overlay; - if (b == null) { - bitmap = null; + videoEditTextureView = videoView; + cropTransform = transform; + bitmapRotation = rotation; + bitmap = b; + areaView.setIsVideo(videoEditTextureView != null); + if (b == null && videoView == null) { state = null; imageView.setImageDrawable(null); } else { - bitmap = b; + int w = getCurrentWidth(); + int h = getCurrentHeight(); if (state == null || !same) { - state = new CropState(bitmap, rotation); - imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + state = new CropState(w, h, 0); + areaView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { reset(); - imageView.getViewTreeObserver().removeOnPreDrawListener(this); + if (restoreState != null) { + if (restoreState.lockedAspectRatio > 0.0001f) { + areaView.setLockedAspectRatio(restoreState.lockedAspectRatio); + if (listener != null) { + listener.onAspectLock(true); + } + } + setFreeform(restoreState.freeform); + + float aspect = areaView.getAspectRatio(); + float stateWidth; + float stateHeight; + int rotatedW; + int rotatedH; + if (restoreState.transformRotation == 90 || restoreState.transformRotation == 270) { + aspect = 1.0f / aspect; + stateWidth = state.height; + stateHeight = state.width; + rotatedW = h; + rotatedH = w; + } else { + stateWidth = state.width; + stateHeight = state.height; + rotatedW = w; + rotatedH = h; + } + + int orientation = restoreState.transformRotation; + boolean fform = freeform; + if (freeform && areaView.getLockAspectRatio() > 0) { + areaView.setLockedAspectRatio(1.0f / areaView.getLockAspectRatio()); + areaView.setActualRect(areaView.getLockAspectRatio()); + fform = false; + } else { + areaView.setBitmap(getCurrentWidth(), getCurrentHeight(), (orientation + state.getBaseRotation()) % 180 != 0, freeform); + } + state.reset(areaView, orientation, fform); + + areaView.setActualRect(aspect * restoreState.cropPw / restoreState.cropPh); + state.mirrored = restoreState.mirrored; + state.rotate(restoreState.cropRotate, 0, 0); + state.translate(restoreState.cropPx * rotatedW * state.minimumScale, restoreState.cropPy * rotatedH * state.minimumScale); + float ts = Math.max(areaView.getCropWidth() / stateWidth, areaView.getCropHeight() / stateHeight) / state.minimumScale; + state.scale(restoreState.cropScale * ts, 0, 0); + updateMatrix(); + + if (listener != null) { + listener.onChange(false); + } + } + areaView.getViewTreeObserver().removeOnPreDrawListener(this); return false; } }); } else { - state.updateBitmap(bitmap, rotation); + state.update(w, h, rotation); } - imageView.setImageBitmap(bitmap); + imageView.setImageBitmap(videoView == null ? bitmap : null); } } public void willShow() { - areaView.setFrameVisibility(true); + areaView.setFrameVisibility(true, false); areaView.setDimVisibility(true); areaView.invalidate(); } - public void hideBackView() { - backView.setVisibility(INVISIBLE); - } - - public void showBackView() { - backView.setVisibility(VISIBLE); - } - public void setFreeform(boolean fform) { areaView.setFreeform(fform); freeform = fform; } + public void onShow() { + isVisible = true; + } + + public void onHide() { + videoEditTextureView = null; + paintingOverlay = null; + isVisible = false; + } + public void show() { - backView.setVisibility(VISIBLE); - imageView.setVisibility(VISIBLE); + updateCropTransform(); + //imageView.setVisibility(VISIBLE); areaView.setDimVisibility(true); - areaView.setFrameVisibility(true); + areaView.setFrameVisibility(true, true); areaView.invalidate(); } public void hide() { - backView.setVisibility(INVISIBLE); imageView.setVisibility(INVISIBLE); areaView.setDimVisibility(false); - areaView.setFrameVisibility(false); + areaView.setFrameVisibility(false, false); areaView.invalidate(); } public void reset() { areaView.resetAnimator(); - - areaView.setBitmap(bitmap, state.getBaseRotation() % 180 != 0, freeform); + areaView.setBitmap(getCurrentWidth(), getCurrentHeight(), state.getBaseRotation() % 180 != 0, freeform); areaView.setLockedAspectRatio(freeform ? 0.0f : 1.0f); state.reset(areaView, 0, freeform); + state.mirrored = false; areaView.getCropRect(initialAreaRect); updateMatrix(); @@ -357,13 +418,6 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen } public void updateMatrix() { - presentationMatrix.reset(); - presentationMatrix.postTranslate(-state.getWidth() / 2, -state.getHeight() / 2); - presentationMatrix.postRotate(state.getOrientation()); - state.getConcatMatrix(presentationMatrix); - presentationMatrix.postTranslate(areaView.getCropCenterX(), areaView.getCropCenterY()); - imageView.setImageMatrix(presentationMatrix); - overlayMatrix.reset(); if (state.getBaseRotation() == 90 || state.getBaseRotation() == 270) { overlayMatrix.postTranslate(-state.getHeight() / 2, -state.getWidth() / 2); @@ -373,13 +427,16 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen overlayMatrix.postRotate(state.getOrientationOnly()); state.getConcatMatrix(overlayMatrix); overlayMatrix.postTranslate(areaView.getCropCenterX(), areaView.getCropCenterY()); + if ((!freeform || isVisible)) { + updateCropTransform(); + listener.onUpdate(); + } invalidate(); } private void fillAreaView(RectF targetRect, boolean allowZoomOut) { final float[] currentScale = new float[]{1.0f}; - float scale = Math.max(targetRect.width() / areaView.getCropWidth(), - targetRect.height() / areaView.getCropHeight()); + float scale = Math.max(targetRect.width() / areaView.getCropWidth(), targetRect.height() / areaView.getCropHeight()); float newScale = state.getScale() * scale; boolean ensureFit = false; @@ -387,7 +444,7 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen scale = MAX_SCALE / state.getScale(); ensureFit = true; } - float statusBarHeight = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + float statusBarHeight = (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); final float x = (targetRect.centerX() - imageView.getWidth() / 2) / areaView.getCropWidth() * state.getOrientedWidth(); final float y = (targetRect.centerY() - (imageView.getHeight() - bottomPadding + statusBarHeight) / 2) / areaView.getCropHeight() * state.getOrientedHeight(); @@ -406,8 +463,9 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - if (animEnsureFit) + if (animEnsureFit) { fitContentInBounds(false, false, true); + } } }); areaView.fill(targetRect, animator, true); @@ -421,8 +479,7 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen float scaledX = (contentRect.width() - scaledW) / 2.0f; float scaledY = (contentRect.height() - scaledH) / 2.0f; - contentRect.set(contentRect.left + scaledX, contentRect.top + scaledY, - contentRect.left + scaledX + scaledW, contentRect.top + scaledY + scaledH); + contentRect.set(contentRect.left + scaledX, contentRect.top + scaledY, contentRect.left + scaledX + scaledW, contentRect.top + scaledY + scaledH); return scale * ratio; } @@ -551,13 +608,13 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen float ratio = boundsRect.width() / scaleWidthToMaxSize(boundsRect, contentRect); targetScale = fitScale(contentRect, scale, ratio); } - fitTranslation(contentRect, boundsRect, targetTranslation, radians); } else if (maximize && rotationStartScale > 0) { float ratio = boundsRect.width() / scaleWidthToMaxSize(boundsRect, contentRect); float newScale = state.getScale() * ratio; - if (newScale < rotationStartScale) + if (newScale < rotationStartScale) { ratio = 1.0f; + } targetScale = fitScale(contentRect, scale, ratio); fitTranslation(contentRect, boundsRect, targetTranslation, radians); @@ -571,8 +628,7 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen final float animDX = dx; final float animDY = dy; - if (Math.abs(animScale - 1.0f) < EPSILON - && Math.abs(animDX) < EPSILON && Math.abs(animDY) < EPSILON) { + if (Math.abs(animScale - 1.0f) < EPSILON && Math.abs(animDX) < EPSILON && Math.abs(animDY) < EPSILON) { return; } @@ -600,8 +656,9 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen public void onAnimationEnd(Animator animation) { animating = false; - if (!fast) + if (!fast) { fitContentInBounds(allowScale, maximize, animated, true); + } } }); animator.setInterpolator(areaView.getInterpolator()); @@ -614,9 +671,36 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen } } - public void rotate90Degrees() { + private int getCurrentWidth() { + if (videoEditTextureView != null) { + return videoEditTextureView.getVideoWidth(); + } + return bitmapRotation == 90 || bitmapRotation == 270 ? bitmap.getHeight() : bitmap.getWidth(); + } + + private int getCurrentHeight() { + if (videoEditTextureView != null) { + return videoEditTextureView.getVideoHeight(); + } + return bitmapRotation == 90 || bitmapRotation == 270 ? bitmap.getWidth() : bitmap.getHeight(); + } + + public boolean mirror() { if (state == null) { - return; + return false; + } + state.mirror(); + updateMatrix(); + if (listener != null) { + float orientation = (state.getOrientation() - state.getBaseRotation()) % 360; + listener.onChange(!state.hasChanges() && orientation == 0 && areaView.getLockAspectRatio() == 0 && !state.mirrored); + } + return state.mirrored; + } + + public boolean rotate90Degrees() { + if (state == null) { + return false; } areaView.resetAnimator(); @@ -630,14 +714,17 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen areaView.setActualRect(areaView.getLockAspectRatio()); fform = false; } else { - areaView.setBitmap(bitmap, (orientation + state.getBaseRotation()) % 180 != 0, freeform); + areaView.setBitmap(getCurrentWidth(), getCurrentHeight(), (orientation + state.getBaseRotation()) % 180 != 0, freeform); } state.reset(areaView, orientation, fform); updateMatrix(); + fitContentInBounds(true, false, false); - if (listener != null) - listener.onChange(orientation == 0 && areaView.getLockAspectRatio() == 0); + if (listener != null) { + listener.onChange(orientation == 0 && areaView.getLockAspectRatio() == 0 && !state.mirrored); + } + return state.getOrientationOnly() != 0; } @Override @@ -646,21 +733,25 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen return true; } boolean result = false; - if (areaView.onTouchEvent(event)) + if (areaView.onTouchEvent(event)) { return true; + } switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_DOWN: { onScrollChangeBegan(); break; + } case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_CANCEL: { onScrollChangeEnded(); break; + } } try { result = detector.onTouchEvent(event); } catch (Exception ignore) { + } return result; } @@ -704,12 +795,19 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen if (animating) { return; } - state.translate(dx, dy); updateMatrix(); } public void onFling(float startX, float startY, float velocityX, float velocityY) { + + } + + @Override + public void onTapUp() { + if (listener != null) { + listener.onTapUp(); + } } public void onScrollChangeBegan() { @@ -736,10 +834,11 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen } float newScale = state.getScale() * scale; - if (newScale > MAX_SCALE) + if (newScale > MAX_SCALE) { scale = MAX_SCALE / state.getScale(); + } - float statusBarHeight = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + float statusBarHeight = (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); float pivotX = (x - imageView.getWidth() / 2) / areaView.getCropWidth() * state.getOrientedWidth(); float pivotY = (y - (imageView.getHeight() - bottomPadding - statusBarHeight) / 2) / areaView.getCropHeight() * state.getOrientedHeight(); @@ -769,8 +868,7 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen fitContentInBounds(true, true, false); } - @SuppressLint("WrongThread") - private void editBitmap(String path, Bitmap b, Canvas canvas, Bitmap canvasBitmap, Bitmap.CompressFormat format, float scale, ArrayList entities, boolean clear) { + public static void editBitmap(Context context, String path, Bitmap b, Canvas canvas, Bitmap canvasBitmap, Bitmap.CompressFormat format, Matrix stateMatrix, int contentWidth, int contentHeight, float stateScale, float rotation, float orientationOnly, float scale, boolean mirror, ArrayList entities, boolean clear) { try { if (clear) { canvasBitmap.eraseColor(0); @@ -778,22 +876,28 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen if (b == null) { b = BitmapFactory.decodeFile(path); } - float sc = Math.max(b.getWidth(), b.getHeight()) / (float) Math.max(bitmap.getWidth(), bitmap.getHeight()); + float sc = Math.max(b.getWidth(), b.getHeight()) / (float) Math.max(contentWidth, contentHeight); Matrix matrix = new Matrix(); matrix.postTranslate(-b.getWidth() / 2, -b.getHeight() / 2); + if (mirror) { + matrix.postScale(-1, 1); + } matrix.postScale(1.0f / sc, 1.0f / sc); - matrix.postRotate(state.getOrientationOnly()); - state.getConcatMatrix(matrix); + matrix.postRotate(orientationOnly); + matrix.postConcat(stateMatrix); matrix.postScale(scale, scale); matrix.postTranslate(canvasBitmap.getWidth() / 2, canvasBitmap.getHeight() / 2); - canvas.drawBitmap(b, matrix, new Paint(FILTER_BITMAP_FLAG)); + canvas.drawBitmap(b, matrix, new Paint(Paint.FILTER_BITMAP_FLAG)); FileOutputStream stream = new FileOutputStream(new File(path)); canvasBitmap.compress(format, 87, stream); stream.close(); if (entities != null && !entities.isEmpty()) { float[] point = new float[4]; - float newScale = 1.0f / sc * scale * state.scale; + float newScale = 1.0f / sc * scale * stateScale; + float widthScale = b.getWidth() / (float) canvasBitmap.getWidth(); + newScale *= widthScale; + TextPaintView textPaintView = null; for (int a = 0, N = entities.size(); a < N; a++) { VideoEditedInfo.MediaEntity entity = entities.get(a); @@ -803,12 +907,27 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen point[3] = entity.textViewY * b.getHeight(); matrix.mapPoints(point); - float widthScale = b.getWidth() / (float) canvasBitmap.getWidth(); - newScale *= widthScale; if (entity.type == 0) { entity.viewWidth = entity.viewHeight = canvasBitmap.getWidth() / 2; } else if (entity.type == 1) { entity.fontSize = canvasBitmap.getWidth() / 9; + if (textPaintView == null) { + textPaintView = new TextPaintView(context, new Point(0, 0), entity.fontSize, "", new Swatch(Color.BLACK, 0.85f, 0.1f), 0); + textPaintView.setMaxWidth(canvasBitmap.getWidth() - 20); + } + int type; + if ((entity.subType & 1) != 0) { + type = 0; + } else if ((entity.subType & 4) != 0) { + type = 2; + } else { + type = 1; + } + textPaintView.setType(type); + textPaintView.setText(entity.text); + textPaintView.measure(MeasureSpec.makeMeasureSpec(canvasBitmap.getWidth(), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(canvasBitmap.getHeight(), MeasureSpec.AT_MOST)); + entity.viewWidth = textPaintView.getMeasuredWidth(); + entity.viewHeight = textPaintView.getMeasuredHeight(); } entity.scale *= newScale; @@ -823,7 +942,7 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen entity.textViewWidth = entity.viewWidth / (float) canvasBitmap.getWidth(); entity.textViewHeight = entity.viewHeight / (float) canvasBitmap.getHeight(); - entity.rotation -= (state.getRotation() + state.getOrientationOnly()) * (Math.PI / 180); + entity.rotation -= (rotation + orientationOnly) * (Math.PI / 180); } } @@ -833,48 +952,154 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen } } - public Bitmap getResult(MediaController.MediaEditState editState) { - if (state == null || !state.hasChanges() && state.getBaseRotation() < EPSILON && freeform) { - return bitmap; + RectF cropRect = new RectF(); + RectF sizeRect = new RectF(0, 0, RESULT_SIDE, RESULT_SIDE); + + private void updateCropTransform() { + if (cropTransform == null) { + return; + } + areaView.getCropRect(cropRect); + float w = scaleWidthToMaxSize(cropRect, sizeRect); + int width = (int) Math.ceil(w); + int height = (int) (Math.ceil(width / areaView.getAspectRatio())); + float scale = width / areaView.getCropWidth(); + + state.matrix.getValues(values); + float sc = state.minimumScale * scale; + + int transformRotation = state.getOrientationOnly(); + while (transformRotation < 0) { + transformRotation += 360; + } + int sw; + int sh; + if (transformRotation == 90 || transformRotation == 270) { + sw = (int) state.height; + sh = (int) state.width; + } else { + sw = (int) state.width; + sh = (int) state.height; + } + float cropPw = (float) (width / Math.ceil(sw * sc)); + float cropPh = (float) (height / Math.ceil(sh * sc)); + if (cropPw > 1 || cropPh > 1) { + float max = Math.max(cropPw, cropPh); + cropPw /= max; + cropPh /= max; + } + + float realMininumScale; + RectF rect = areaView.getTargetRectToFill(sw / (float) sh); + if (freeform) { + realMininumScale = rect.width() / sw; + } else { + float wScale = rect.width() / sw; + float hScale = rect.height() / sh; + realMininumScale = Math.max(wScale, hScale); + } + + float cropScale = state.scale / realMininumScale; + float trueCropScale = state.scale / state.minimumScale; + float cropPx = values[2] / sw / state.scale; + float cropPy = values[5] / sh / state.scale; + float cropRotate = state.rotation; + + RectF targetRect = areaView.getTargetRectToFill(); + float tx = areaView.getCropCenterX() - targetRect.centerX(); + float ty = areaView.getCropCenterY() - targetRect.centerY(); + cropTransform.setViewTransform(state.mirrored || state.hasChanges() || state.getBaseRotation() >= EPSILON, cropPx, cropPy, cropRotate, state.getOrientationOnly(), cropScale, trueCropScale, state.minimumScale / realMininumScale, cropPw, cropPh, tx, ty, state.mirrored); + } + + public static String getCopy(String path) { + File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); + try { + AndroidUtilities.copyFile(new File(path), f); + } catch (Exception e) { + FileLog.e(e); + } + return f.getAbsolutePath(); + } + + public void makeCrop(MediaController.MediaEditState editState) { + if (state == null) { + return; } - RectF cropRect = new RectF(); areaView.getCropRect(cropRect); - RectF sizeRect = new RectF(0, 0, RESULT_SIDE, RESULT_SIDE); float w = scaleWidthToMaxSize(cropRect, sizeRect); int width = (int) Math.ceil(w); int height = (int) (Math.ceil(width / areaView.getAspectRatio())); float scale = width / areaView.getCropWidth(); - Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - - Matrix matrix = new Matrix(); - matrix.postTranslate(-state.getWidth() / 2, -state.getHeight() / 2); - matrix.postRotate(state.getOrientation()); - state.getConcatMatrix(matrix); - matrix.postScale(scale, scale); - matrix.postTranslate(width / 2, height / 2); - - Canvas canvas = new Canvas(resultBitmap); - if (editState.paintPath != null) { - editBitmap(editState.paintPath, null, canvas, resultBitmap, Bitmap.CompressFormat.PNG, scale, null, false); - if (!editState.paintPath.equals(editState.fullPaintPath)) { - editBitmap(editState.fullPaintPath, null, canvas, resultBitmap, Bitmap.CompressFormat.PNG, scale, editState.mediaEntities, true); + Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(resultBitmap); + + String path = getCopy(editState.paintPath); + + if (editState.croppedPaintPath != null) { + new File(editState.croppedPaintPath).delete(); + editState.croppedPaintPath = null; } - } - if (editState.filterPath != null) { - if (editState.croppedPath == null) { - File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); - editState.croppedPath = f.getAbsolutePath(); + editState.croppedPaintPath = path; + if (editState.mediaEntities != null && !editState.mediaEntities.isEmpty()) { + editState.croppedMediaEntities = new ArrayList<>(editState.mediaEntities.size()); + for (int a = 0, N = editState.mediaEntities.size(); a < N; a++) { + editState.croppedMediaEntities.add(editState.mediaEntities.get(a).copy()); + } + } else { + editState.croppedMediaEntities = null; } - Bitmap b = ImageLoader.loadBitmap(editState.getPath(), null, bitmap.getWidth(), bitmap.getHeight(), true); - editBitmap(editState.croppedPath, b, canvas, resultBitmap, Bitmap.CompressFormat.JPEG, scale, null, false); + + editBitmap(getContext(), path, null, canvas, resultBitmap, Bitmap.CompressFormat.PNG, state.matrix, getCurrentWidth(), getCurrentHeight(), state.scale, state.rotation, state.getOrientationOnly(), scale, false, editState.croppedMediaEntities, false); } - canvas.drawBitmap(bitmap, matrix, new Paint(FILTER_BITMAP_FLAG)); - return resultBitmap; + if (editState.cropState == null) { + editState.cropState = new MediaController.CropState(); + } + state.matrix.getValues(values); + float sc = state.minimumScale * scale; + + editState.cropState.transformRotation = state.getOrientationOnly(); + if (BuildVars.LOGS_ENABLED) { + FileLog.d("set transformRotation = " + editState.cropState.transformRotation); + } + while (editState.cropState.transformRotation < 0) { + editState.cropState.transformRotation += 360; + } + int sw; + int sh; + if (editState.cropState.transformRotation == 90 || editState.cropState.transformRotation == 270) { + sw = (int) state.height; + sh = (int) state.width; + } else { + sw = (int) state.width; + sh = (int) state.height; + } + editState.cropState.cropPw = (float) (width / Math.ceil(sw * sc)); + editState.cropState.cropPh = (float) (height / Math.ceil(sh * sc)); + if (editState.cropState.cropPw > 1 || editState.cropState.cropPh > 1) { + float max = Math.max(editState.cropState.cropPw, editState.cropState.cropPh); + editState.cropState.cropPw /= max; + editState.cropState.cropPh /= max; + } + editState.cropState.cropScale = state.scale * Math.min(sw / areaView.getCropWidth(), sh / areaView.getCropHeight()); + editState.cropState.cropPx = values[2] / sw / state.scale; + editState.cropState.cropPy = values[5] / sh / state.scale; + editState.cropState.cropRotate = state.rotation; + editState.cropState.stateScale = state.scale; + editState.cropState.mirrored = state.mirrored; + + editState.cropState.scale = scale; + editState.cropState.matrix = state.matrix; + editState.cropState.width = width; + editState.cropState.height = height; + editState.cropState.freeform = freeform; + editState.cropState.lockedAspectRatio = areaView.getLockAspectRatio(); + + editState.cropState.initied = true; } private void setLockedAspectRatio(float aspectRatio) { @@ -893,7 +1118,7 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen if (state == null) { return; } - if (areaView.getLockAspectRatio() > 0) { + /*if (areaView.getLockAspectRatio() > 0) { areaView.setLockedAspectRatio(0); if (listener != null) { @@ -901,7 +1126,7 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen } return; - } + }*/ if (hasAspectRatioDialog) { return; @@ -998,4 +1223,9 @@ public class CropView extends FrameLayout implements CropAreaView.AreaViewListen public float getCropHeight() { return areaView.getCropHeight(); } + + public RectF getActualRect() { + areaView.getCropRect(cropRect); + return cropRect; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextBoldCursor.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextBoldCursor.java index 720070b16..57167600b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextBoldCursor.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextBoldCursor.java @@ -22,6 +22,8 @@ import android.graphics.Rect; import android.os.Build; import android.os.SystemClock; import androidx.annotation.Keep; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; @@ -731,7 +733,7 @@ public class EditTextBoldCursor extends EditText { super.onInitializeAccessibilityNodeInfo(info); info.setClassName("android.widget.EditText"); if (hintLayout != null) { - info.setContentDescription(hintLayout.getText()); + AccessibilityNodeInfoCompat.wrap(info).setHintText(hintLayout.getText()); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextCaption.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextCaption.java index a827a5a9b..ec42ad056 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextCaption.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextCaption.java @@ -14,6 +14,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.os.Build; +import android.os.Bundle; import android.text.Editable; import android.text.Layout; import android.text.Spanned; @@ -32,6 +33,8 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.inputmethod.EditorInfo; import android.widget.FrameLayout; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + import org.jetbrains.annotations.NotNull; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.FileLog; @@ -48,8 +51,12 @@ import tw.nekomimi.nekogram.transtale.Translator; import tw.nekomimi.nekogram.transtale.TranslatorKt; import tw.nekomimi.nekogram.utils.AlertUtil; +import java.util.List; + public class EditTextCaption extends EditTextBoldCursor { + private static final int ACCESSIBILITY_ACTION_SHARE = 0x10000000; + private String caption; private StaticLayout captionLayout; private int userNameLength; @@ -62,6 +69,7 @@ public class EditTextCaption extends EditTextBoldCursor { private int selectionStart = -1; private int selectionEnd = -1; private boolean allowTextEntitiesIntersection; + private float offsetY; public interface EditTextCaptionDelegate { void onSpansChanged(); @@ -392,40 +400,7 @@ public class EditTextCaption extends EditTextBoldCursor { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (item.getItemId() == R.id.menu_regular) { - makeSelectedRegular(); - mode.finish(); - return true; - } else if (item.getItemId() == R.id.menu_bold) { - makeSelectedBold(); - mode.finish(); - return true; - } else if (item.getItemId() == R.id.menu_italic) { - makeSelectedItalic(); - mode.finish(); - return true; - } else if (item.getItemId() == R.id.menu_mono) { - makeSelectedMono(); - mode.finish(); - return true; - } else if (item.getItemId() == R.id.menu_link) { - makeSelectedUrl(); - mode.finish(); - return true; - } else if (item.getItemId() == R.id.menu_mention) { - makeSelectedMention(); - mode.finish(); - return true; - } else if (item.getItemId() == R.id.menu_strike) { - makeSelectedStrike(); - mode.finish(); - return true; - } else if (item.getItemId() == R.id.menu_underline) { - makeSelectedUnderline(); - mode.finish(); - return true; - } else if (item.getItemId() == R.id.menu_translate) { - makeSelectedTranslate(); + if (performMenuAction(item.getItemId())) { mode.finish(); return true; } @@ -478,6 +453,35 @@ public class EditTextCaption extends EditTextBoldCursor { } } + private boolean performMenuAction(int itemId) { + if (itemId == R.id.menu_regular) { + makeSelectedRegular(); + return true; + } else if (itemId == R.id.menu_bold) { + makeSelectedBold(); + return true; + } else if (itemId == R.id.menu_italic) { + makeSelectedItalic(); + return true; + } else if (itemId == R.id.menu_mono) { + makeSelectedMono(); + return true; + } else if (itemId == R.id.menu_link) { + makeSelectedUrl(); + return true; + } else if (itemId == R.id.menu_strike) { + makeSelectedStrike(); + return true; + } else if (itemId == R.id.menu_underline) { + makeSelectedUnderline(); + return true; + } else if (itemId == R.id.menu_translate) { + makeSelectedTranslate(); + return true; + } + return false; + } + @Override public ActionMode startActionMode(final ActionMode.Callback callback, int type) { return super.startActionMode(overrideCallback(callback), type); @@ -537,8 +541,19 @@ public class EditTextCaption extends EditTextBoldCursor { invalidate(); } + public void setOffsetY(float offset) { + this.offsetY = offset; + invalidate(); + } + + public float getOffsetY() { + return offsetY; + } + @Override protected void onDraw(Canvas canvas) { + canvas.save(); + canvas.translate(0, offsetY); super.onDraw(canvas); try { if (captionLayout != null && userNameLength == length()) { @@ -554,17 +569,37 @@ public class EditTextCaption extends EditTextBoldCursor { } catch (Exception e) { FileLog.e(e); } + canvas.restore(); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); + final AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info); if (!TextUtils.isEmpty(caption)) { - if (Build.VERSION.SDK_INT >= 26) { - info.setHintText(caption); - } else { - info.setText(info.getText() + ", " + caption); + infoCompat.setHintText(caption); + } + final List actions = infoCompat.getActionList(); + for (int i = 0, size = actions.size(); i < size; i++) { + final AccessibilityNodeInfoCompat.AccessibilityActionCompat action = actions.get(i); + if (action.getId() == ACCESSIBILITY_ACTION_SHARE) { + infoCompat.removeAction(action); + break; } } + if (hasSelection()) { + infoCompat.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.menu_bold, LocaleController.getString("Bold", R.string.Bold))); + infoCompat.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.menu_italic, LocaleController.getString("Italic", R.string.Italic))); + infoCompat.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.menu_mono, LocaleController.getString("Mono", R.string.Mono))); + infoCompat.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.menu_strike, LocaleController.getString("Strike", R.string.Strike))); + infoCompat.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.menu_underline, LocaleController.getString("Underline", R.string.Underline))); + infoCompat.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.menu_link, LocaleController.getString("CreateLink", R.string.CreateLink))); + infoCompat.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.menu_regular, LocaleController.getString("Regular", R.string.Regular))); + } + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + return performMenuAction(action) || super.performAccessibilityAction(action, arguments); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmbedBottomSheet.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmbedBottomSheet.java index 7b076d692..314615d2f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmbedBottomSheet.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmbedBottomSheet.java @@ -28,6 +28,7 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; +import android.view.MotionEvent; import android.view.OrientationEventListener; import android.view.Surface; import android.view.TextureView; @@ -148,9 +149,9 @@ public class EmbedBottomSheet extends BottomSheet { " \"height\" : \"100%%\"," + " \"playerVars\" : {" + " \"start\" : %2$d," + - " \"rel\" : 0," + + " \"rel\" : 1," + " \"showinfo\" : 0," + - " \"modestbranding\" : 1," + + " \"modestbranding\" : 0," + " \"iv_load_policy\" : 3," + " \"autohide\" : 1," + " \"autoplay\" : 1," + @@ -216,12 +217,6 @@ public class EmbedBottomSheet extends BottomSheet { @Override public boolean onPreDraw() { videoView.getViewTreeObserver().removeOnPreDrawListener(this); - /*AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() {*/ - - /*} - }, 100);*/ return true; } }); @@ -232,15 +227,17 @@ public class EmbedBottomSheet extends BottomSheet { @SuppressLint("StaticFieldLeak") private static EmbedBottomSheet instance; - public static void show(Context context, String title, String description, String originalUrl, final String url, int w, int h) { - show(context, title, description, originalUrl, url, w, h, -1); + public static void show(Context context, String title, String description, String originalUrl, final String url, int w, int h, boolean keyboardVisible) { + show(context, title, description, originalUrl, url, w, h, -1, keyboardVisible); } - public static void show(Context context, String title, String description, String originalUrl, final String url, int w, int h, int seekTime) { + public static void show(Context context, String title, String description, String originalUrl, final String url, int w, int h, int seekTime, boolean keyboardVisible) { if (instance != null) { instance.destroy(); } - new EmbedBottomSheet(context, title, description, originalUrl, url, w, h, seekTime).show(); + EmbedBottomSheet sheet = new EmbedBottomSheet(context, title, description, originalUrl, url, w, h, seekTime); + sheet.setCalcMandatoryInsets(keyboardVisible); + sheet.show(); } @SuppressLint("SetJavaScriptEnabled") @@ -312,7 +309,20 @@ public class EmbedBottomSheet extends BottomSheet { containerLayout.setOnTouchListener((v, event) -> true); setCustomView(containerLayout); - webView = new WebView(context); + webView = new WebView(context) { + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean result = super.onTouchEvent(event); + if (result) { + if (event.getAction() == MotionEvent.ACTION_UP) { + setDisableScroll(false); + } else { + setDisableScroll(true); + } + } + return result; + } + }; webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setDomStorageEnabled(true); if (Build.VERSION.SDK_INT >= 17) { @@ -370,7 +380,6 @@ public class EmbedBottomSheet extends BottomSheet { super.onLoadResource(view, url); } - @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); @@ -381,6 +390,15 @@ public class EmbedBottomSheet extends BottomSheet { pipButton.setAlpha(1.0f); } } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (isYouTube) { + Browser.openUrl(view.getContext(), url); + return true; + } + return super.shouldOverrideUrlLoading(view, url); + } }); containerLayout.addView(webView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 0, 0, 48 + 36 + (hasDescription ? 22 : 0))); @@ -400,7 +418,7 @@ public class EmbedBottomSheet extends BottomSheet { } videoView.loadVideo(null, null, null, null, false); HashMap args = new HashMap<>(); - args.put("Referer", "http://youtube.com"); + args.put("Referer", ApplicationLoader.applicationContext.getPackageName()); try { webView.loadUrl(embedUrl, args); } catch (Exception e) { @@ -490,9 +508,6 @@ public class EmbedBottomSheet extends BottomSheet { Rect rect = PipVideoView.getPipRect(aspectRatio); float scale = rect.width / textureView.getWidth(); - if (Build.VERSION.SDK_INT >= 21) { - rect.y += AndroidUtilities.statusBarHeight; - } AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( @@ -546,9 +561,6 @@ public class EmbedBottomSheet extends BottomSheet { TextureView textureView = videoView.getTextureView(); ImageView textureImageView = videoView.getTextureImageView(); float scale = rect.width / textureView.getLayoutParams().width; - if (Build.VERSION.SDK_INT >= 21) { - rect.y += AndroidUtilities.statusBarHeight; - } textureImageView.setScaleX(scale); textureImageView.setScaleY(scale); textureImageView.setTranslationX(rect.x); @@ -718,6 +730,7 @@ public class EmbedBottomSheet extends BottomSheet { pipButton = new ImageView(context); pipButton.setScaleType(ImageView.ScaleType.CENTER); pipButton.setImageResource(R.drawable.video_pip); + pipButton.setContentDescription(LocaleController.getString("AccDescrPipMode", R.string.AccDescrPipMode)); pipButton.setEnabled(false); pipButton.setAlpha(0.5f); pipButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogTextBlue4), PorterDuff.Mode.SRC_IN)); @@ -790,6 +803,7 @@ public class EmbedBottomSheet extends BottomSheet { ImageView copyButton = new ImageView(context); copyButton.setScaleType(ImageView.ScaleType.CENTER); copyButton.setImageResource(R.drawable.video_copy); + copyButton.setContentDescription(LocaleController.getString("CopyLink", R.string.CopyLink)); copyButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogTextBlue4), PorterDuff.Mode.SRC_IN)); copyButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_dialogButtonSelector), 0)); imageButtonsContainer.addView(copyButton, LayoutHelper.createFrame(48, 48, Gravity.TOP | Gravity.LEFT)); @@ -824,10 +838,15 @@ public class EmbedBottomSheet extends BottomSheet { dismiss(); }); + boolean canHandleUrl = videoView.canHandleUrl(embedUrl); + if (!canHandleUrl) { + videoView.setVisibility(View.INVISIBLE); + } + setDelegate(new BottomSheet.BottomSheetDelegate() { @Override public void onOpenAnimationEnd() { - boolean handled = videoView.loadVideo(embedUrl, null, null, openUrl, true); + boolean handled = canHandleUrl && videoView.loadVideo(embedUrl, null, null, openUrl, true); if (handled) { progressBar.setVisibility(View.INVISIBLE); webView.setVisibility(View.INVISIBLE); @@ -846,7 +865,7 @@ public class EmbedBottomSheet extends BottomSheet { } videoView.loadVideo(null, null, null, null, false); HashMap args = new HashMap<>(); - args.put("Referer", "http://youtube.com"); + args.put("Referer", ApplicationLoader.applicationContext.getPackageName()); try { String currentYoutubeId = videoView.getYoutubeId(); if (currentYoutubeId != null) { @@ -922,11 +941,13 @@ public class EmbedBottomSheet extends BottomSheet { }; String currentYoutubeId = videoView.getYouTubeVideoId(embedUrl); - if (currentYoutubeId != null) { + if (currentYoutubeId != null || !canHandleUrl) { progressBar.setVisibility(View.VISIBLE); webView.setVisibility(View.VISIBLE); imageButtonsContainer.setVisibility(View.VISIBLE); - progressBarBlackBackground.setVisibility(View.VISIBLE); + if (currentYoutubeId != null) { + progressBarBlackBackground.setVisibility(View.VISIBLE); + } copyTextButton.setVisibility(View.INVISIBLE); webView.setKeepScreenOn(true); videoView.setVisibility(View.INVISIBLE); @@ -935,7 +956,7 @@ public class EmbedBottomSheet extends BottomSheet { if (videoView.getTextureImageView() != null) { videoView.getTextureImageView().setVisibility(View.INVISIBLE); } - /*if ("disabled".equals(MessagesController.getInstance(currentAccount).youtubePipType)) { + /*if (currentYoutubeId != null && "disabled".equals(MessagesController.getInstance(currentAccount).youtubePipType)) { pipButton.setVisibility(View.GONE); }*/ } 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 a48f90e7d..e9e02d41e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java @@ -3006,21 +3006,24 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific if (hasRecent) { gifRecentTabNum = gifTabsCount++; - gifTabs.addIconTab(0, gifIcons[0]); + gifTabs.addIconTab(0, gifIcons[0]).setContentDescription(LocaleController.getString("RecentStickers", R.string.RecentStickers)); } gifTrendingTabNum = gifTabsCount++; - gifTabs.addIconTab(1, gifIcons[1]); + gifTabs.addIconTab(1, gifIcons[1]).setContentDescription(LocaleController.getString("FeaturedGifs", R.string.FeaturedGifs)); gifFirstEmojiTabNum = gifTabsCount; final int hPadding = AndroidUtilities.dp(13); final int vPadding = AndroidUtilities.dp(11); final List gifSearchEmojies = MessagesController.getInstance(currentAccount).gifSearchEmojies; for (int i = 0, N = gifSearchEmojies.size(); i < N; i++) { - final Emoji.EmojiDrawable emojiDrawable = Emoji.getEmojiDrawable(gifSearchEmojies.get(i)); + final String emoji = gifSearchEmojies.get(i); + final Emoji.EmojiDrawable emojiDrawable = Emoji.getEmojiDrawable(emoji); if (emojiDrawable != null) { gifTabsCount++; - gifTabs.addIconTab(3 + i, emojiDrawable).setPadding(hPadding, vPadding, hPadding, vPadding); + final ImageView iconTab = gifTabs.addIconTab(3 + i, emojiDrawable); + iconTab.setPadding(hPadding, vPadding, hPadding, vPadding); + iconTab.setContentDescription(emoji); } } @@ -3405,14 +3408,14 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific } private void updateRecentGifs() { - final boolean wasEmpty = recentGifs.isEmpty(); - int prevHash = MediaDataController.calcDocumentsHash(recentGifs); + final int prevSize = recentGifs.size(); + int prevHash = MediaDataController.calcDocumentsHash(recentGifs, Integer.MAX_VALUE); recentGifs = MediaDataController.getInstance(currentAccount).getRecentGifs(); - int newHash = MediaDataController.calcDocumentsHash(recentGifs); - if (gifTabs != null && wasEmpty && !recentGifs.isEmpty() || !wasEmpty && recentGifs.isEmpty()) { + int newHash = MediaDataController.calcDocumentsHash(recentGifs, Integer.MAX_VALUE); + if (gifTabs != null && prevSize == 0 && !recentGifs.isEmpty() || prevSize != 0 && recentGifs.isEmpty()) { updateGifTabs(); } - if (prevHash != newHash && gifAdapter != null) { + if ((prevSize != recentGifs.size() || prevHash != newHash) && gifAdapter != null) { gifAdapter.notifyDataSetChanged(); } } @@ -4345,7 +4348,7 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { - return false; + return holder.getItemViewType() == 0; } @Override @@ -4371,7 +4374,6 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific switch (viewType) { case 0: ContextLinkCell cell = new ContextLinkCell(context); - cell.setContentDescription(LocaleController.getString("AttachGif", R.string.AttachGif)); cell.setCanPreviewGif(true); view = cell; break; @@ -4709,85 +4711,6 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific } } - private class GifSavedAdapter extends RecyclerListView.SelectionAdapter { - - private Context mContext; - - public GifSavedAdapter(Context context) { - mContext = context; - } - - @Override - public boolean isEnabled(RecyclerView.ViewHolder holder) { - return false; - } - - @Override - public int getItemCount() { - return recentGifs.size() + 1; - } - - @Override - public long getItemId(int i) { - return i; - } - - @Override - public int getItemViewType(int position) { - if (position == 0) { - return 1; - } - return 0; - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view; - switch (viewType) { - case 0: - ContextLinkCell cell = new ContextLinkCell(mContext); - cell.setContentDescription(LocaleController.getString("AttachGif", R.string.AttachGif)); - cell.setCanPreviewGif(true); - view = cell; - break; - case 1: - default: - view = new View(getContext()); - view.setLayoutParams(new RecyclerView.LayoutParams(LayoutHelper.MATCH_PARENT, searchFieldHeight)); - break; - } - return new RecyclerListView.Holder(view); - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - switch (holder.getItemViewType()) { - case 0: { - TLRPC.Document document = recentGifs.get(position - 1); - if (document != null) { - ((ContextLinkCell) holder.itemView).setGif(document, false); - } - break; - } - } - } - - @Override - public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { - if (holder.itemView instanceof ContextLinkCell) { - ContextLinkCell cell = (ContextLinkCell) holder.itemView; - ImageReceiver imageReceiver = cell.getPhotoImage(); - if (pager.getCurrentItem() == 1) { - imageReceiver.setAllowStartAnimation(true); - imageReceiver.startAnimation(); - } else { - imageReceiver.setAllowStartAnimation(false); - imageReceiver.stopAnimation(); - } - } - } - } - private class GifSearchPreloader { private final List loadingKeys = new ArrayList<>(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmptyTextProgressView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmptyTextProgressView.java index af193a78f..9748e5e76 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmptyTextProgressView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmptyTextProgressView.java @@ -34,29 +34,30 @@ public class EmptyTextProgressView extends FrameLayout { super(context); progressBar = new RadialProgressView(context); - progressBar.setVisibility(INVISIBLE); addView(progressBar, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT)); textView = new TextView(context); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); textView.setTextColor(Theme.getColor(Theme.key_emptyListPlaceholder)); textView.setGravity(Gravity.CENTER); - textView.setVisibility(INVISIBLE); textView.setPadding(AndroidUtilities.dp(20), 0, AndroidUtilities.dp(20), 0); textView.setText(LocaleController.getString("NoResult", R.string.NoResult)); addView(textView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT)); + progressBar.setAlpha(0f); + textView.setAlpha(0f); + setOnTouchListener((v, event) -> true); } public void showProgress() { - textView.setVisibility(INVISIBLE); - progressBar.setVisibility(VISIBLE); + textView.animate().alpha(0f).setDuration(150).start(); + progressBar.animate().alpha(1f).setDuration(150).start(); } public void showTextView() { - textView.setVisibility(VISIBLE); - progressBar.setVisibility(INVISIBLE); + textView.animate().alpha(1f).setDuration(150).start(); + progressBar.animate().alpha(0f).setDuration(150).start(); } public void setText(String text) { 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 4a3ae4490..939f73a3e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java @@ -14,6 +14,7 @@ import android.util.SparseIntArray; import org.telegram.messenger.AndroidUtilities; import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; public class ExtendedGridLayoutManager extends GridLayoutManager { @@ -167,4 +168,14 @@ public class ExtendedGridLayoutManager extends GridLayoutManager { protected int getFlowItemCount() { return getItemCount(); } + + @Override + public int getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state) { + return state.getItemCount(); + } + + @Override + public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state) { + return 1; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/FilterGLThread.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/FilterGLThread.java index 40fb609e4..435fe0386 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/FilterGLThread.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/FilterGLThread.java @@ -13,6 +13,8 @@ import org.telegram.messenger.DispatchQueue; import org.telegram.messenger.FileLog; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; import java.util.concurrent.CountDownLatch; import javax.microedition.khronos.egl.EGL10; @@ -58,6 +60,8 @@ public class FilterGLThread extends DispatchQueue { private int videoWidth; private int videoHeight; + private FloatBuffer textureBuffer; + private boolean renderDataSet; private long lastRenderCallTime; @@ -68,12 +72,35 @@ public class FilterGLThread extends DispatchQueue { private FilterGLThreadVideoDelegate videoDelegate; - public FilterGLThread(SurfaceTexture surface, Bitmap bitmap, int bitmapOrientation) { + public FilterGLThread(SurfaceTexture surface, Bitmap bitmap, int bitmapOrientation, boolean mirror) { super("PhotoFilterGLThread", false); surfaceTexture = surface; currentBitmap = bitmap; orientation = bitmapOrientation; filterShaders = new FilterShaders(false); + + float[] textureCoordinates = { + 0.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 1.0f, + }; + if (mirror) { + float temp = textureCoordinates[2]; + textureCoordinates[2] = textureCoordinates[0]; + textureCoordinates[0] = temp; + + temp = textureCoordinates[6]; + textureCoordinates[6] = textureCoordinates[4]; + textureCoordinates[4] = temp; + } + + ByteBuffer bb = ByteBuffer.allocateDirect(textureCoordinates.length * 4); + bb.order(ByteOrder.nativeOrder()); + textureBuffer = bb.asFloatBuffer(); + textureBuffer.put(textureCoordinates); + textureBuffer.position(0); + start(); } @@ -248,6 +275,8 @@ public class FilterGLThread extends DispatchQueue { videoHeight /= 2; } renderDataSet = false; + setRenderData(); + drawRunnable.run(); }); } @@ -271,6 +300,16 @@ public class FilterGLThread extends DispatchQueue { } } + private void setRenderData() { + if (renderDataSet || videoWidth <= 0 || videoHeight <= 0) { + return; + } + filterShaders.setRenderData(currentBitmap, orientation, videoTexture[0], videoWidth, videoHeight); + renderDataSet = true; + renderBufferWidth = filterShaders.getRenderBufferWidth(); + renderBufferHeight = filterShaders.getRenderBufferHeight(); + } + private Runnable drawRunnable = new Runnable() { @Override public void run() { @@ -290,20 +329,7 @@ public class FilterGLThread extends DispatchQueue { if (updateSurface) { videoSurfaceTexture.updateTexImage(); videoSurfaceTexture.getTransformMatrix(videoTextureMatrix); - if (videoWidth == 0 || videoHeight == 0) { - videoWidth = surfaceWidth; - videoHeight = surfaceHeight; - if (videoWidth > 1280 || videoHeight > 1280) { - videoWidth /= 2; - videoHeight /= 2; - } - } - if (!renderDataSet && videoWidth > 0 && videoHeight > 0) { - filterShaders.setRenderData(currentBitmap, orientation, videoTexture[0], videoWidth, videoHeight); - renderDataSet = true; - renderBufferWidth = filterShaders.getRenderBufferWidth(); - renderBufferHeight = filterShaders.getRenderBufferHeight(); - } + setRenderData(); updateSurface = false; filterShaders.onVideoFrameUpdate(videoTextureMatrix); videoFrameAvailable = true; @@ -315,6 +341,7 @@ public class FilterGLThread extends DispatchQueue { if (videoDelegate == null || videoFrameAvailable) { GLES20.glViewport(0, 0, renderBufferWidth, renderBufferHeight); + filterShaders.drawSkinSmoothPass(); filterShaders.drawEnhancePass(); if (videoDelegate == null) { filterShaders.drawSharpenPass(); @@ -332,7 +359,7 @@ public class FilterGLThread extends DispatchQueue { GLES20.glUniform1i(simpleSourceImageHandle, 0); GLES20.glEnableVertexAttribArray(simpleInputTexCoordHandle); - GLES20.glVertexAttribPointer(simpleInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, filterShaders.getTextureBuffer()); + GLES20.glVertexAttribPointer(simpleInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer != null ? textureBuffer : filterShaders.getTextureBuffer()); GLES20.glEnableVertexAttribArray(simplePositionHandle); GLES20.glVertexAttribPointer(simplePositionHandle, 2, GLES20.GL_FLOAT, false, 8, filterShaders.getVertexBuffer()); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); @@ -341,6 +368,9 @@ public class FilterGLThread extends DispatchQueue { }; private Bitmap getRenderBufferBitmap() { + if (renderBufferWidth == 0 || renderBufferHeight == 0) { + return null; + } ByteBuffer buffer = ByteBuffer.allocateDirect(renderBufferWidth * renderBufferHeight * 4); GLES20.glReadPixels(0, 0, renderBufferWidth, renderBufferHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer); Bitmap bitmap = Bitmap.createBitmap(renderBufferWidth, renderBufferHeight, Bitmap.Config.ARGB_8888); @@ -349,13 +379,13 @@ public class FilterGLThread extends DispatchQueue { } public Bitmap getTexture() { - if (!initied) { + if (!initied || !isAlive()) { return null; } final CountDownLatch countDownLatch = new CountDownLatch(1); final Bitmap[] object = new Bitmap[1]; try { - postRunnable(() -> { + if (postRunnable(() -> { GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, filterShaders.getRenderFrameBuffer()); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, filterShaders.getRenderTexture(blurred ? 0 : 1), 0); GLES20.glClear(0); @@ -363,8 +393,9 @@ public class FilterGLThread extends DispatchQueue { countDownLatch.countDown(); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glClear(0); - }); - countDownLatch.await(); + })) { + countDownLatch.await(); + } } catch (Exception e) { FileLog.e(e); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/FilterShaders.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/FilterShaders.java index 686acd014..a117f9d3a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/FilterShaders.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/FilterShaders.java @@ -2,6 +2,7 @@ package org.telegram.ui.Components; import android.graphics.Bitmap; import android.graphics.Matrix; +import android.graphics.PointF; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.opengl.GLUtils; @@ -15,483 +16,685 @@ import org.telegram.messenger.Utilities; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Locale; import javax.microedition.khronos.opengles.GL10; public class FilterShaders { + private static final String YUCIHighPassSkinSmoothingMaskBoostFilterFragmentShaderCode = + "precision lowp float;" + + "varying highp vec2 texCoord;" + + "uniform sampler2D sourceImage;" + + "void main() {" + + "vec4 color = texture2D(sourceImage, texCoord);" + + "float hardLightColor = color.b;" + + "for (int i = 0; i < 3; ++i)" + + "{" + + "if (hardLightColor < 0.5) {" + + "hardLightColor = hardLightColor * hardLightColor * 2.0;" + + "} else {" + + "hardLightColor = 1.0 - (1.0 - hardLightColor) * (1.0 - hardLightColor) * 2.0;" + + "}" + + "}" + + "float k = 255.0 / (164.0 - 75.0);" + + "hardLightColor = (hardLightColor - 75.0 / 255.0) * k;" + + "gl_FragColor = vec4(vec3(hardLightColor), color.a);" + + "}"; + + private static final String highpassSkinSmoothingCompositingFilterFragmentShaderString = + "%1$s\n" + + "precision lowp float;" + + "varying highp vec2 texCoord;" + + "varying highp vec2 texCoord2;" + + "uniform %2$s sourceImage;" + + "uniform sampler2D toneCurveTexture;" + + "uniform sampler2D inputImageTexture3;" + + "uniform lowp float mixturePercent;" + + "void main() {" + + "vec4 image = texture2D(sourceImage, texCoord);" + + "vec4 mask = texture2D(inputImageTexture3, texCoord2);" + + "float redCurveValue = texture2D(toneCurveTexture, vec2(image.r, 0.0)).r;" + + "float greenCurveValue = texture2D(toneCurveTexture, vec2(image.g, 0.0)).g;" + + "float blueCurveValue = texture2D(toneCurveTexture, vec2(image.b, 0.0)).b;" + + "vec4 result = vec4(redCurveValue, greenCurveValue, blueCurveValue, image.a);" + + "vec4 tone = mix(image, result, mixturePercent);" + + "gl_FragColor = vec4(mix(image.rgb, tone.rgb, 1.0 - mask.b), 1.0);" + + "}"; + + private static final String greenAndBlueChannelOverlayFragmentShaderCode = + "%1$s\n" + + "precision lowp float;" + + "varying highp vec2 texCoord;" + + "uniform %2$s sourceImage;" + + "void main() {" + + "vec4 inp = texture2D(sourceImage, texCoord);" + + "vec4 image = vec4(inp.rgb * pow(2.0, -1.0), inp.w);" + + "vec4 base = vec4(image.g, image.g, image.g, 1.0);" + + "vec4 overlay = vec4(image.b, image.b, image.b, 1.0);" + + "float ba = 2.0 * overlay.b * base.b + overlay.b * (1.0 - base.a) + base.b * (1.0 - overlay.a);" + + "gl_FragColor = vec4(ba,ba,ba,image.a);" + + "}"; + + private static final String stillImageHighPassFilterFragmentShaderCode = + "precision lowp float;" + + "varying highp vec2 texCoord;" + + "varying highp vec2 texCoord2;" + + "uniform sampler2D sourceImage;" + + "uniform sampler2D inputImageTexture2;" + + "void main() {" + + "vec4 image = texture2D(sourceImage, texCoord);" + + "vec4 blurredImage = texture2D(inputImageTexture2, texCoord2);" + + "gl_FragColor = vec4((image.rgb - blurredImage.rgb + vec3(0.5,0.5,0.5)), image.a);" + + "}"; + private static final String radialBlurFragmentShaderCode = "varying highp vec2 texCoord;" + - "uniform sampler2D sourceImage;" + - "uniform sampler2D inputImageTexture2;" + - "uniform lowp float excludeSize;" + - "uniform lowp vec2 excludePoint;" + - "uniform lowp float excludeBlurSize;" + - "uniform highp float aspectRatio;" + - "void main() {" + - "lowp vec4 sharpImageColor = texture2D(sourceImage, texCoord);" + - "lowp vec4 blurredImageColor = texture2D(inputImageTexture2, texCoord);" + - "highp vec2 texCoordToUse = vec2(texCoord.x, (texCoord.y * aspectRatio + 0.5 - 0.5 * aspectRatio));" + - "highp float distanceFromCenter = distance(excludePoint, texCoordToUse);" + - "gl_FragColor = mix(sharpImageColor, blurredImageColor, smoothstep(excludeSize - excludeBlurSize, excludeSize, distanceFromCenter));" + - "}"; + "uniform sampler2D sourceImage;" + + "uniform sampler2D inputImageTexture2;" + + "uniform lowp float excludeSize;" + + "uniform lowp vec2 excludePoint;" + + "uniform lowp float excludeBlurSize;" + + "uniform highp float aspectRatio;" + + "void main() {" + + "lowp vec4 sharpImageColor = texture2D(sourceImage, texCoord);" + + "lowp vec4 blurredImageColor = texture2D(inputImageTexture2, texCoord);" + + "highp vec2 texCoordToUse = vec2(texCoord.x, (texCoord.y * aspectRatio + 0.5 - 0.5 * aspectRatio));" + + "highp float distanceFromCenter = distance(excludePoint, texCoordToUse);" + + "gl_FragColor = mix(sharpImageColor, blurredImageColor, smoothstep(excludeSize - excludeBlurSize, excludeSize, distanceFromCenter));" + + "}"; private static final String linearBlurFragmentShaderCode = "varying highp vec2 texCoord;" + - "uniform sampler2D sourceImage;" + - "uniform sampler2D inputImageTexture2;" + - "uniform lowp float excludeSize;" + - "uniform lowp vec2 excludePoint;" + - "uniform lowp float excludeBlurSize;" + - "uniform highp float angle;" + - "uniform highp float aspectRatio;" + - "void main() {" + - "lowp vec4 sharpImageColor = texture2D(sourceImage, texCoord);" + - "lowp vec4 blurredImageColor = texture2D(inputImageTexture2, texCoord);" + - "highp vec2 texCoordToUse = vec2(texCoord.x, (texCoord.y * aspectRatio + 0.5 - 0.5 * aspectRatio));" + - "highp float distanceFromCenter = abs((texCoordToUse.x - excludePoint.x) * aspectRatio * cos(angle) + (texCoordToUse.y - excludePoint.y) * sin(angle));" + - "gl_FragColor = mix(sharpImageColor, blurredImageColor, smoothstep(excludeSize - excludeBlurSize, excludeSize, distanceFromCenter));" + - "}"; - - private static final String blurVertexShaderCode = - "attribute vec4 position;" + - "attribute vec4 inputTexCoord;" + - "uniform highp float texelWidthOffset;" + - "uniform highp float texelHeightOffset;" + - "varying vec2 blurCoordinates[9];" + - "void main() {" + - "gl_Position = position;" + - "vec2 singleStepOffset = vec2(texelWidthOffset, texelHeightOffset);" + - "blurCoordinates[0] = inputTexCoord.xy;" + - "blurCoordinates[1] = inputTexCoord.xy + singleStepOffset * 1.458430;" + - "blurCoordinates[2] = inputTexCoord.xy - singleStepOffset * 1.458430;" + - "blurCoordinates[3] = inputTexCoord.xy + singleStepOffset * 3.403985;" + - "blurCoordinates[4] = inputTexCoord.xy - singleStepOffset * 3.403985;" + - "blurCoordinates[5] = inputTexCoord.xy + singleStepOffset * 5.351806;" + - "blurCoordinates[6] = inputTexCoord.xy - singleStepOffset * 5.351806;" + - "blurCoordinates[7] = inputTexCoord.xy + singleStepOffset * 7.302940;" + - "blurCoordinates[8] = inputTexCoord.xy - singleStepOffset * 7.302940;" + - "}"; - - private static final String blurFragmentShaderCode = "uniform sampler2D sourceImage;" + - "varying highp vec2 blurCoordinates[9];" + - "void main() {" + - "lowp vec4 sum = vec4(0.0);" + - "sum += texture2D(sourceImage, blurCoordinates[0]) * 0.133571;" + - "sum += texture2D(sourceImage, blurCoordinates[1]) * 0.233308;" + - "sum += texture2D(sourceImage, blurCoordinates[2]) * 0.233308;" + - "sum += texture2D(sourceImage, blurCoordinates[3]) * 0.135928;" + - "sum += texture2D(sourceImage, blurCoordinates[4]) * 0.135928;" + - "sum += texture2D(sourceImage, blurCoordinates[5]) * 0.051383;" + - "sum += texture2D(sourceImage, blurCoordinates[6]) * 0.051383;" + - "sum += texture2D(sourceImage, blurCoordinates[7]) * 0.012595;" + - "sum += texture2D(sourceImage, blurCoordinates[8]) * 0.012595;" + - "gl_FragColor = sum;" + - "}"; + "uniform sampler2D inputImageTexture2;" + + "uniform lowp float excludeSize;" + + "uniform lowp vec2 excludePoint;" + + "uniform lowp float excludeBlurSize;" + + "uniform highp float angle;" + + "uniform highp float aspectRatio;" + + "void main() {" + + "lowp vec4 sharpImageColor = texture2D(sourceImage, texCoord);" + + "lowp vec4 blurredImageColor = texture2D(inputImageTexture2, texCoord);" + + "highp vec2 texCoordToUse = vec2(texCoord.x, (texCoord.y * aspectRatio + 0.5 - 0.5 * aspectRatio));" + + "highp float distanceFromCenter = abs((texCoordToUse.x - excludePoint.x) * aspectRatio * cos(angle) + (texCoordToUse.y - excludePoint.y) * sin(angle));" + + "gl_FragColor = mix(sharpImageColor, blurredImageColor, smoothstep(excludeSize - excludeBlurSize, excludeSize, distanceFromCenter));" + + "}"; + + private static String vertexShaderForOptimizedBlurOfRadius(int blurRadius, float sigma) { + float[] standardGaussianWeights = new float[blurRadius + 1]; + float sumOfWeights = 0.0f; + for (int currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) { + standardGaussianWeights[currentGaussianWeightIndex] = (float) ((1.0 / Math.sqrt(2.0 * Math.PI * Math.pow(sigma, 2.0))) * Math.exp(-Math.pow(currentGaussianWeightIndex, 2.0) / (2.0 * Math.pow(sigma, 2.0)))); + if (currentGaussianWeightIndex == 0) { + sumOfWeights += standardGaussianWeights[currentGaussianWeightIndex]; + } else { + sumOfWeights += 2.0 * standardGaussianWeights[currentGaussianWeightIndex]; + } + } + for (int currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) { + standardGaussianWeights[currentGaussianWeightIndex] = standardGaussianWeights[currentGaussianWeightIndex] / sumOfWeights; + } + int numberOfOptimizedOffsets = Math.min(blurRadius / 2 + (blurRadius % 2), 7); + float[] optimizedGaussianOffsets = new float[numberOfOptimizedOffsets]; + for (int currentOptimizedOffset = 0; currentOptimizedOffset < numberOfOptimizedOffsets; currentOptimizedOffset++) { + float firstWeight = standardGaussianWeights[currentOptimizedOffset * 2 + 1]; + float secondWeight = standardGaussianWeights[currentOptimizedOffset * 2 + 2]; + float optimizedWeight = firstWeight + secondWeight; + optimizedGaussianOffsets[currentOptimizedOffset] = (firstWeight * (currentOptimizedOffset * 2 + 1) + secondWeight * (currentOptimizedOffset * 2 + 2)) / optimizedWeight; + } + StringBuilder shaderString = new StringBuilder(); + shaderString.append("attribute vec4 position;\n"); + shaderString.append("attribute vec4 inputTexCoord;\n"); + shaderString.append("uniform float texelWidthOffset;\n"); + shaderString.append("uniform float texelHeightOffset;\n"); + shaderString.append(String.format(Locale.US, "varying vec2 blurCoordinates[%d];\n", 1 + (numberOfOptimizedOffsets * 2))); + shaderString.append("void main()\n"); + shaderString.append("{\n"); + shaderString.append("gl_Position = position;\n"); + shaderString.append("vec2 singleStepOffset = vec2(texelWidthOffset, texelHeightOffset);\n"); + shaderString.append("blurCoordinates[0] = inputTexCoord.xy;\n"); + for (int currentOptimizedOffset = 0; currentOptimizedOffset < numberOfOptimizedOffsets; currentOptimizedOffset++) { + shaderString.append(String.format(Locale.US, + "blurCoordinates[%d] = inputTexCoord.xy + singleStepOffset * %f;\n" + + "blurCoordinates[%d] = inputTexCoord.xy - singleStepOffset * %f;\n", ((currentOptimizedOffset * 2) + 1), optimizedGaussianOffsets[currentOptimizedOffset], ((currentOptimizedOffset * 2) + 2), optimizedGaussianOffsets[currentOptimizedOffset])); + } + shaderString.append("}"); + return shaderString.toString(); + } + + private static String fragmentShaderForOptimizedBlurOfRadius(int blurRadius, float sigma) { + float[] standardGaussianWeights = new float[blurRadius + 1]; + float sumOfWeights = 0.0f; + for (int currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) { + standardGaussianWeights[currentGaussianWeightIndex] = (float) ((1.0 / Math.sqrt(2.0 * Math.PI * Math.pow(sigma, 2.0))) * Math.exp(-Math.pow(currentGaussianWeightIndex, 2.0) / (2.0 * Math.pow(sigma, 2.0)))); + if (currentGaussianWeightIndex == 0) { + sumOfWeights += standardGaussianWeights[currentGaussianWeightIndex]; + } else { + sumOfWeights += 2.0 * standardGaussianWeights[currentGaussianWeightIndex]; + } + } + for (int currentGaussianWeightIndex = 0; currentGaussianWeightIndex < blurRadius + 1; currentGaussianWeightIndex++) { + standardGaussianWeights[currentGaussianWeightIndex] = standardGaussianWeights[currentGaussianWeightIndex] / sumOfWeights; + } + int numberOfOptimizedOffsets = Math.min(blurRadius / 2 + (blurRadius % 2), 7); + int trueNumberOfOptimizedOffsets = blurRadius / 2 + (blurRadius % 2); + + StringBuilder shaderString = new StringBuilder(); + + shaderString.append("uniform sampler2D sourceImage;\n"); + shaderString.append("uniform highp float texelWidthOffset;\n"); + shaderString.append("uniform highp float texelHeightOffset;\n"); + shaderString.append(String.format(Locale.US, "varying highp vec2 blurCoordinates[%d];\n", 1 + (numberOfOptimizedOffsets * 2))); + shaderString.append("void main()\n"); + shaderString.append("{\n"); + shaderString.append("lowp vec4 sum = vec4(0.0);\n"); + shaderString.append(String.format(Locale.US, "sum += texture2D(sourceImage, blurCoordinates[0]) * %f;\n", standardGaussianWeights[0])); + + for (int currentBlurCoordinateIndex = 0; currentBlurCoordinateIndex < numberOfOptimizedOffsets; currentBlurCoordinateIndex++) { + float firstWeight = standardGaussianWeights[currentBlurCoordinateIndex * 2 + 1]; + float secondWeight = standardGaussianWeights[currentBlurCoordinateIndex * 2 + 2]; + float optimizedWeight = firstWeight + secondWeight; + shaderString.append(String.format(Locale.US, "sum += texture2D(sourceImage, blurCoordinates[%d]) * %f;\n", ((currentBlurCoordinateIndex * 2) + 1), optimizedWeight)); + shaderString.append(String.format(Locale.US, "sum += texture2D(sourceImage, blurCoordinates[%d]) * %f;\n", ((currentBlurCoordinateIndex * 2) + 2), optimizedWeight)); + } + + if (trueNumberOfOptimizedOffsets > numberOfOptimizedOffsets) { + shaderString.append("highp vec2 singleStepOffset = vec2(texelWidthOffset, texelHeightOffset);\n"); + + for (int currentOverlowTextureRead = numberOfOptimizedOffsets; currentOverlowTextureRead < trueNumberOfOptimizedOffsets; currentOverlowTextureRead++) { + float firstWeight = standardGaussianWeights[currentOverlowTextureRead * 2 + 1]; + float secondWeight = standardGaussianWeights[currentOverlowTextureRead * 2 + 2]; + float optimizedWeight = firstWeight + secondWeight; + float optimizedOffset = (firstWeight * (currentOverlowTextureRead * 2 + 1) + secondWeight * (currentOverlowTextureRead * 2 + 2)) / optimizedWeight; + shaderString.append(String.format(Locale.US, "sum += texture2D(sourceImage, blurCoordinates[0] + singleStepOffset * %f) * %f;\n", optimizedOffset, optimizedWeight)); + shaderString.append(String.format(Locale.US, "sum += texture2D(sourceImage, blurCoordinates[0] - singleStepOffset * %f) * %f;\n", optimizedOffset, optimizedWeight)); + } + } + + shaderString.append("gl_FragColor = sum;\n"); + shaderString.append("}\n"); + + return shaderString.toString(); + } + + private static class BlurProgram { + + private String vertexShaderCode; + private String fragmentShaderCode; + + public int blurShaderProgram; + public int blurPositionHandle; + public int blurInputTexCoordHandle; + public int blurSourceImageHandle; + public int blurWidthHandle; + public int blurHeightHandle; + + public BlurProgram(float radius, float sigma, boolean calc) { + int calculatedSampleRadius; + if (calc) { + sigma = Math.round(radius); + calculatedSampleRadius = 0; + if (sigma >= 1) { + float minimumWeightToFindEdgeOfSamplingArea = 1.0f / 256.0f; + calculatedSampleRadius = (int) Math.floor(Math.sqrt(-2.0 * Math.pow(sigma, 2.0) * Math.log(minimumWeightToFindEdgeOfSamplingArea * Math.sqrt(2.0 * Math.PI * Math.pow(sigma, 2.0))))); + calculatedSampleRadius += calculatedSampleRadius % 2; + } + } else { + calculatedSampleRadius = (int) radius; + } + fragmentShaderCode = fragmentShaderForOptimizedBlurOfRadius(calculatedSampleRadius, sigma); + vertexShaderCode = vertexShaderForOptimizedBlurOfRadius(calculatedSampleRadius, sigma); + } + + public void destroy() { + if (blurShaderProgram != 0) { + GLES20.glDeleteProgram(blurShaderProgram); + blurShaderProgram = 0; + } + } + + public boolean create() { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); + + if (vertexShader == 0 || fragmentShader == 0) { + return false; + } + blurShaderProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(blurShaderProgram, vertexShader); + GLES20.glAttachShader(blurShaderProgram, fragmentShader); + GLES20.glBindAttribLocation(blurShaderProgram, 0, "position"); + GLES20.glBindAttribLocation(blurShaderProgram, 1, "inputTexCoord"); + + GLES20.glLinkProgram(blurShaderProgram); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(blurShaderProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + GLES20.glDeleteProgram(blurShaderProgram); + blurShaderProgram = 0; + } else { + blurPositionHandle = GLES20.glGetAttribLocation(blurShaderProgram, "position"); + blurInputTexCoordHandle = GLES20.glGetAttribLocation(blurShaderProgram, "inputTexCoord"); + blurSourceImageHandle = GLES20.glGetUniformLocation(blurShaderProgram, "sourceImage"); + blurWidthHandle = GLES20.glGetUniformLocation(blurShaderProgram, "texelWidthOffset"); + blurHeightHandle = GLES20.glGetUniformLocation(blurShaderProgram, "texelHeightOffset"); + } + return true; + } + } private static final String simpleVertexVideoShaderCode = "attribute vec4 position;" + - "uniform mat4 videoMatrix;" + - "attribute vec4 inputTexCoord;" + - "varying vec2 texCoord;" + - "void main() {" + - "gl_Position = position;" + - "texCoord = vec2(videoMatrix * inputTexCoord).xy;" + - "}"; + "uniform mat4 videoMatrix;" + + "attribute vec4 inputTexCoord;" + + "varying vec2 texCoord;" + + "void main() {" + + "gl_Position = position;" + + "texCoord = vec2(videoMatrix * inputTexCoord).xy;" + + "}"; - private static final String rgbToHsvFragmentVideoShaderCode = - "#extension GL_OES_EGL_image_external : require\n" + - "precision highp float;" + - "varying vec2 texCoord;" + - "uniform samplerExternalOES sourceImage;" + - "vec3 rgb_to_hsv(vec3 c) {" + - "vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);" + - "vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy);" + - "vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx);" + - "float d = q.x - min(q.w, q.y);" + - "float e = 1.0e-10;" + - "return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);" + - "}" + - "void main() {" + - "vec4 texel = texture2D(sourceImage, texCoord);" + - "gl_FragColor = vec4(rgb_to_hsv(texel.rgb), texel.a);" + - "}"; + private static final String simpleTwoTextureVertexVideoShaderCode = + "attribute vec4 position;" + + "uniform mat4 videoMatrix;" + + "attribute vec4 inputTexCoord;" + + "varying vec2 texCoord;" + + "varying vec2 texCoord2;" + + "void main() {" + + "gl_Position = position;" + + "texCoord = vec2(videoMatrix * inputTexCoord).xy;" + + "texCoord2 = inputTexCoord.xy;" + + "}"; private static final String rgbToHsvFragmentShaderCode = + "%1$s\n" + "precision highp float;" + - "varying vec2 texCoord;" + - "uniform sampler2D sourceImage;" + - "vec3 rgb_to_hsv(vec3 c) {" + - "vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);" + - "vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy);" + - "vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx);" + - "float d = q.x - min(q.w, q.y);" + - "float e = 1.0e-10;" + - "return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);" + - "}" + - "void main() {" + - "vec4 texel = texture2D(sourceImage, texCoord);" + - "gl_FragColor = vec4(rgb_to_hsv(texel.rgb), texel.a);" + - "}"; + "varying vec2 texCoord;" + + "uniform %2$s sourceImage;" + + "vec3 rgb_to_hsv(vec3 c) {" + + "vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);" + + "vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy);" + + "vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx);" + + "float d = q.x - min(q.w, q.y);" + + "float e = 1.0e-10;" + + "return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);" + + "}" + + "void main() {" + + "vec4 texel = texture2D(sourceImage, texCoord);" + + "gl_FragColor = vec4(rgb_to_hsv(texel.rgb), texel.a);" + + "}"; private static final String enhanceFragmentShaderCode = "precision highp float;" + - "varying vec2 texCoord;" + - "uniform sampler2D sourceImage;" + - "uniform sampler2D inputImageTexture2;" + - "uniform float intensity;" + - "float enhance(float value) {" + - "const vec2 offset = vec2(0.001953125, 0.03125);" + - "value = value + offset.x;" + - "vec2 coord = (clamp(texCoord, 0.125, 1.0 - 0.125001) - 0.125) * 4.0;" + - "vec2 frac = fract(coord);" + - "coord = floor(coord);" + - "float p00 = float(coord.y * 4.0 + coord.x) * 0.0625 + offset.y;" + - "float p01 = float(coord.y * 4.0 + coord.x + 1.0) * 0.0625 + offset.y;" + - "float p10 = float((coord.y + 1.0) * 4.0 + coord.x) * 0.0625 + offset.y;" + - "float p11 = float((coord.y + 1.0) * 4.0 + coord.x + 1.0) * 0.0625 + offset.y;" + - "vec3 c00 = texture2D(inputImageTexture2, vec2(value, p00)).rgb;" + - "vec3 c01 = texture2D(inputImageTexture2, vec2(value, p01)).rgb;" + - "vec3 c10 = texture2D(inputImageTexture2, vec2(value, p10)).rgb;" + - "vec3 c11 = texture2D(inputImageTexture2, vec2(value, p11)).rgb;" + - "float c1 = ((c00.r - c00.g) / (c00.b - c00.g));" + - "float c2 = ((c01.r - c01.g) / (c01.b - c01.g));" + - "float c3 = ((c10.r - c10.g) / (c10.b - c10.g));" + - "float c4 = ((c11.r - c11.g) / (c11.b - c11.g));" + - "float c1_2 = mix(c1, c2, frac.x);" + - "float c3_4 = mix(c3, c4, frac.x);" + - "return mix(c1_2, c3_4, frac.y);" + - "}" + - "vec3 hsv_to_rgb(vec3 c) {" + - "vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);" + - "vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);" + - "return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);" + - "}" + - "void main() {" + - "vec4 texel = texture2D(sourceImage, texCoord);" + - "vec4 hsv = texel;" + - "hsv.y = min(1.0, hsv.y * 1.2);" + - "hsv.z = min(1.0, enhance(hsv.z) * 1.1);" + - "gl_FragColor = vec4(hsv_to_rgb(mix(texel.xyz, hsv.xyz, intensity)), texel.w);" + - "}"; + "varying vec2 texCoord;" + + "uniform sampler2D sourceImage;" + + "uniform sampler2D inputImageTexture2;" + + "uniform float intensity;" + + "float enhance(float value) {" + + "const vec2 offset = vec2(0.001953125, 0.03125);" + + "value = value + offset.x;" + + "vec2 coord = (clamp(texCoord, 0.125, 1.0 - 0.125001) - 0.125) * 4.0;" + + "vec2 frac = fract(coord);" + + "coord = floor(coord);" + + "float p00 = float(coord.y * 4.0 + coord.x) * 0.0625 + offset.y;" + + "float p01 = float(coord.y * 4.0 + coord.x + 1.0) * 0.0625 + offset.y;" + + "float p10 = float((coord.y + 1.0) * 4.0 + coord.x) * 0.0625 + offset.y;" + + "float p11 = float((coord.y + 1.0) * 4.0 + coord.x + 1.0) * 0.0625 + offset.y;" + + "vec3 c00 = texture2D(inputImageTexture2, vec2(value, p00)).rgb;" + + "vec3 c01 = texture2D(inputImageTexture2, vec2(value, p01)).rgb;" + + "vec3 c10 = texture2D(inputImageTexture2, vec2(value, p10)).rgb;" + + "vec3 c11 = texture2D(inputImageTexture2, vec2(value, p11)).rgb;" + + "float c1 = ((c00.r - c00.g) / (c00.b - c00.g));" + + "float c2 = ((c01.r - c01.g) / (c01.b - c01.g));" + + "float c3 = ((c10.r - c10.g) / (c10.b - c10.g));" + + "float c4 = ((c11.r - c11.g) / (c11.b - c11.g));" + + "float c1_2 = mix(c1, c2, frac.x);" + + "float c3_4 = mix(c3, c4, frac.x);" + + "return mix(c1_2, c3_4, frac.y);" + + "}" + + "vec3 hsv_to_rgb(vec3 c) {" + + "vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);" + + "vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);" + + "return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);" + + "}" + + "void main() {" + + "vec4 texel = texture2D(sourceImage, texCoord);" + + "vec4 hsv = texel;" + + "hsv.y = min(1.0, hsv.y * 1.2);" + + "hsv.z = min(1.0, enhance(hsv.z) * 1.1);" + + "gl_FragColor = vec4(hsv_to_rgb(mix(texel.xyz, hsv.xyz, intensity)), texel.w);" + + "}"; public static final String simpleVertexShaderCode = "attribute vec4 position;" + - "attribute vec2 inputTexCoord;" + - "varying vec2 texCoord;" + - "void main() {" + - "gl_Position = position;" + - "texCoord = inputTexCoord;" + - "}"; + "attribute vec2 inputTexCoord;" + + "varying vec2 texCoord;" + + "void main() {" + + "gl_Position = position;" + + "texCoord = inputTexCoord;" + + "}"; + + public static final String simpleTwoTextureVertexShaderCode = + "attribute vec4 position;" + + "attribute vec2 inputTexCoord;" + + "varying vec2 texCoord;" + + "varying vec2 texCoord2;" + + "void main() {" + + "gl_Position = position;" + + "texCoord = inputTexCoord;" + + "texCoord2 = inputTexCoord;" + + "}"; public static final String simpleFragmentShaderCode = "varying highp vec2 texCoord;" + - "uniform sampler2D sourceImage;" + - "void main() {" + - "gl_FragColor = texture2D(sourceImage, texCoord);" + - "}"; + "uniform sampler2D sourceImage;" + + "void main() {" + + "gl_FragColor = texture2D(sourceImage, texCoord);" + + "}"; private static final String sharpenVertexShaderCode = "attribute vec4 position;" + - "attribute vec2 inputTexCoord;" + - "varying vec2 texCoord;" + + "attribute vec2 inputTexCoord;" + + "varying vec2 texCoord;" + - "uniform highp float inputWidth;" + - "uniform highp float inputHeight;" + - "varying vec2 leftTexCoord;" + - "varying vec2 rightTexCoord;" + - "varying vec2 topTexCoord;" + - "varying vec2 bottomTexCoord;" + + "uniform highp float inputWidth;" + + "uniform highp float inputHeight;" + + "varying vec2 leftTexCoord;" + + "varying vec2 rightTexCoord;" + + "varying vec2 topTexCoord;" + + "varying vec2 bottomTexCoord;" + - "void main() {" + - "gl_Position = position;" + - "texCoord = inputTexCoord;" + - "highp vec2 widthStep = vec2(1.0 / inputWidth, 0.0);" + - "highp vec2 heightStep = vec2(0.0, 1.0 / inputHeight);" + - "leftTexCoord = inputTexCoord - widthStep;" + - "rightTexCoord = inputTexCoord + widthStep;" + - "topTexCoord = inputTexCoord + heightStep;" + - "bottomTexCoord = inputTexCoord - heightStep;" + - "}"; + "void main() {" + + "gl_Position = position;" + + "texCoord = inputTexCoord;" + + "highp vec2 widthStep = vec2(1.0 / inputWidth, 0.0);" + + "highp vec2 heightStep = vec2(0.0, 1.0 / inputHeight);" + + "leftTexCoord = inputTexCoord - widthStep;" + + "rightTexCoord = inputTexCoord + widthStep;" + + "topTexCoord = inputTexCoord + heightStep;" + + "bottomTexCoord = inputTexCoord - heightStep;" + + "}"; private static final String sharpenFragmentShaderCode = "precision highp float;" + - "varying vec2 texCoord;" + - "varying vec2 leftTexCoord;" + - "varying vec2 rightTexCoord;" + - "varying vec2 topTexCoord;" + - "varying vec2 bottomTexCoord;" + - "uniform sampler2D sourceImage;" + - "uniform float sharpen;" + + "varying vec2 texCoord;" + + "varying vec2 leftTexCoord;" + + "varying vec2 rightTexCoord;" + + "varying vec2 topTexCoord;" + + "varying vec2 bottomTexCoord;" + + "uniform sampler2D sourceImage;" + + "uniform float sharpen;" + - "void main() {" + - "vec4 result = texture2D(sourceImage, texCoord);" + + "void main() {" + + "vec4 result = texture2D(sourceImage, texCoord);" + - "vec3 leftTextureColor = texture2D(sourceImage, leftTexCoord).rgb;" + - "vec3 rightTextureColor = texture2D(sourceImage, rightTexCoord).rgb;" + - "vec3 topTextureColor = texture2D(sourceImage, topTexCoord).rgb;" + - "vec3 bottomTextureColor = texture2D(sourceImage, bottomTexCoord).rgb;" + - "result.rgb = result.rgb * (1.0 + 4.0 * sharpen) - (leftTextureColor + rightTextureColor + topTextureColor + bottomTextureColor) * sharpen;" + + "vec3 leftTextureColor = texture2D(sourceImage, leftTexCoord).rgb;" + + "vec3 rightTextureColor = texture2D(sourceImage, rightTexCoord).rgb;" + + "vec3 topTextureColor = texture2D(sourceImage, topTexCoord).rgb;" + + "vec3 bottomTextureColor = texture2D(sourceImage, bottomTexCoord).rgb;" + + "result.rgb = result.rgb * (1.0 + 4.0 * sharpen) - (leftTextureColor + rightTextureColor + topTextureColor + bottomTextureColor) * sharpen;" + - "gl_FragColor = result;" + - "}"; + "gl_FragColor = result;" + + "}"; private static final String toolsFragmentShaderCode = "varying highp vec2 texCoord;" + - "uniform sampler2D sourceImage;" + - "uniform highp float width;" + - "uniform highp float height;" + - "uniform sampler2D curvesImage;" + - "uniform lowp float skipTone;" + - "uniform lowp float shadows;" + - "const mediump vec3 hsLuminanceWeighting = vec3(0.3, 0.3, 0.3);" + - "uniform lowp float highlights;" + - "uniform lowp float contrast;" + - "uniform lowp float fadeAmount;" + - "const mediump vec3 satLuminanceWeighting = vec3(0.2126, 0.7152, 0.0722);" + - "uniform lowp float saturation;" + - "uniform lowp float shadowsTintIntensity;" + - "uniform lowp float highlightsTintIntensity;" + - "uniform lowp vec3 shadowsTintColor;" + - "uniform lowp vec3 highlightsTintColor;" + - "uniform lowp float exposure;" + - "uniform lowp float warmth;" + - "uniform lowp float grain;" + - "const lowp float permTexUnit = 1.0 / 256.0;" + - "const lowp float permTexUnitHalf = 0.5 / 256.0;" + - "const lowp float grainsize = 2.3;" + - "uniform lowp float vignette;" + - "highp float getLuma(highp vec3 rgbP) {" + - "return (0.299 * rgbP.r) + (0.587 * rgbP.g) + (0.114 * rgbP.b);" + - "}" + - "lowp vec3 rgbToHsv(lowp vec3 c) {" + - "highp vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);" + - "highp vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy);" + - "highp vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx);" + - "highp float d = q.x - min(q.w, q.y);" + - "highp float e = 1.0e-10;" + - "return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);" + - "}" + - "lowp vec3 hsvToRgb(lowp vec3 c) {" + - "highp vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);" + - "highp vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);" + - "return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);" + - "}" + - "highp vec3 rgbToHsl(highp vec3 color) {" + - "highp vec3 hsl;" + - "highp float fmin = min(min(color.r, color.g), color.b);" + - "highp float fmax = max(max(color.r, color.g), color.b);" + - "highp float delta = fmax - fmin;" + - "hsl.z = (fmax + fmin) / 2.0;" + - "if (delta == 0.0) {" + + "uniform sampler2D sourceImage;" + + "uniform highp float width;" + + "uniform highp float height;" + + "uniform sampler2D curvesImage;" + + "uniform lowp float skipTone;" + + "uniform lowp float shadows;" + + "const mediump vec3 hsLuminanceWeighting = vec3(0.3, 0.3, 0.3);" + + "uniform lowp float highlights;" + + "uniform lowp float contrast;" + + "uniform lowp float fadeAmount;" + + "const mediump vec3 satLuminanceWeighting = vec3(0.2126, 0.7152, 0.0722);" + + "uniform lowp float saturation;" + + "uniform lowp float shadowsTintIntensity;" + + "uniform lowp float highlightsTintIntensity;" + + "uniform lowp vec3 shadowsTintColor;" + + "uniform lowp vec3 highlightsTintColor;" + + "uniform lowp float exposure;" + + "uniform lowp float warmth;" + + "uniform lowp float grain;" + + "const lowp float permTexUnit = 1.0 / 256.0;" + + "const lowp float permTexUnitHalf = 0.5 / 256.0;" + + "const lowp float grainsize = 2.3;" + + "uniform lowp float vignette;" + + "highp float getLuma(highp vec3 rgbP) {" + + "return (0.299 * rgbP.r) + (0.587 * rgbP.g) + (0.114 * rgbP.b);" + + "}" + + "lowp vec3 rgbToHsv(lowp vec3 c) {" + + "highp vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);" + + "highp vec4 p = c.g < c.b ? vec4(c.bg, K.wz) : vec4(c.gb, K.xy);" + + "highp vec4 q = c.r < p.x ? vec4(p.xyw, c.r) : vec4(c.r, p.yzx);" + + "highp float d = q.x - min(q.w, q.y);" + + "highp float e = 1.0e-10;" + + "return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);" + + "}" + + "lowp vec3 hsvToRgb(lowp vec3 c) {" + + "highp vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);" + + "highp vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);" + + "return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);" + + "}" + + "highp vec3 rgbToHsl(highp vec3 color) {" + + "highp vec3 hsl;" + + "highp float fmin = min(min(color.r, color.g), color.b);" + + "highp float fmax = max(max(color.r, color.g), color.b);" + + "highp float delta = fmax - fmin;" + + "hsl.z = (fmax + fmin) / 2.0;" + + "if (delta == 0.0) {" + "hsl.x = 0.0;" + "hsl.y = 0.0;" + - "} else {" + + "} else {" + "if (hsl.z < 0.5) {" + - "hsl.y = delta / (fmax + fmin);" + + "hsl.y = delta / (fmax + fmin);" + "} else {" + - "hsl.y = delta / (2.0 - fmax - fmin);" + + "hsl.y = delta / (2.0 - fmax - fmin);" + "}" + "highp float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta;" + "highp float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta;" + "highp float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta;" + "if (color.r == fmax) {" + - "hsl.x = deltaB - deltaG;" + + "hsl.x = deltaB - deltaG;" + "} else if (color.g == fmax) {" + - "hsl.x = (1.0 / 3.0) + deltaR - deltaB;" + + "hsl.x = (1.0 / 3.0) + deltaR - deltaB;" + "} else if (color.b == fmax) {" + - "hsl.x = (2.0 / 3.0) + deltaG - deltaR;" + + "hsl.x = (2.0 / 3.0) + deltaG - deltaR;" + "}" + "if (hsl.x < 0.0) {" + - "hsl.x += 1.0;" + + "hsl.x += 1.0;" + "} else if (hsl.x > 1.0) {" + - "hsl.x -= 1.0;" + + "hsl.x -= 1.0;" + "}" + - "}" + - "return hsl;" + - "}" + - "highp float hueToRgb(highp float f1, highp float f2, highp float hue) {" + - "if (hue < 0.0) {" + + "}" + + "return hsl;" + + "}" + + "highp float hueToRgb(highp float f1, highp float f2, highp float hue) {" + + "if (hue < 0.0) {" + "hue += 1.0;" + - "} else if (hue > 1.0) {" + + "} else if (hue > 1.0) {" + "hue -= 1.0;" + - "}" + - "highp float res;" + - "if ((6.0 * hue) < 1.0) {" + + "}" + + "highp float res;" + + "if ((6.0 * hue) < 1.0) {" + "res = f1 + (f2 - f1) * 6.0 * hue;" + - "} else if ((2.0 * hue) < 1.0) {" + + "} else if ((2.0 * hue) < 1.0) {" + "res = f2;" + - "} else if ((3.0 * hue) < 2.0) {" + + "} else if ((3.0 * hue) < 2.0) {" + "res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0;" + - "} else {" + + "} else {" + "res = f1;" + - "} return res;" + - "}" + - "highp vec3 hslToRgb(highp vec3 hsl) {" + - "if (hsl.y == 0.0) {" + + "}" + + "return res;" + + "}" + + "highp vec3 hslToRgb(highp vec3 hsl) {" + + "if (hsl.y == 0.0) {" + "return vec3(hsl.z);" + - "} else {" + + "} else {" + "highp float f2;" + "if (hsl.z < 0.5) {" + - "f2 = hsl.z * (1.0 + hsl.y);" + + "f2 = hsl.z * (1.0 + hsl.y);" + "} else {" + - "f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);" + + "f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);" + "}" + "highp float f1 = 2.0 * hsl.z - f2;" + "return vec3(hueToRgb(f1, f2, hsl.x + (1.0/3.0)), hueToRgb(f1, f2, hsl.x), hueToRgb(f1, f2, hsl.x - (1.0/3.0)));" + - "}" + - "}" + - "highp vec3 rgbToYuv(highp vec3 inP) {" + - "highp float luma = getLuma(inP);" + - "return vec3(luma, (1.0 / 1.772) * (inP.b - luma), (1.0 / 1.402) * (inP.r - luma));" + - "}" + - "lowp vec3 yuvToRgb(highp vec3 inP) {" + - "return vec3(1.402 * inP.b + inP.r, (inP.r - (0.299 * 1.402 / 0.587) * inP.b - (0.114 * 1.772 / 0.587) * inP.g), 1.772 * inP.g + inP.r);" + - "}" + - "lowp float easeInOutSigmoid(lowp float value, lowp float strength) {" + - "if (value > 0.5) {" + + "}" + + "}" + + "highp vec3 rgbToYuv(highp vec3 inP) {" + + "highp float luma = getLuma(inP);" + + "return vec3(luma, (1.0 / 1.772) * (inP.b - luma), (1.0 / 1.402) * (inP.r - luma));" + + "}" + + "lowp vec3 yuvToRgb(highp vec3 inP) {" + + "return vec3(1.402 * inP.b + inP.r, (inP.r - (0.299 * 1.402 / 0.587) * inP.b - (0.114 * 1.772 / 0.587) * inP.g), 1.772 * inP.g + inP.r);" + + "}" + + "lowp float easeInOutSigmoid(lowp float value, lowp float strength) {" + + "if (value > 0.5) {" + "return 1.0 - pow(2.0 - 2.0 * value, 1.0 / (1.0 - strength)) * 0.5;" + - "} else {" + + "} else {" + "return pow(2.0 * value, 1.0 / (1.0 - strength)) * 0.5;" + - "}" + - "}" + - "lowp vec3 applyLuminanceCurve(lowp vec3 pixel) {" + - "highp float index = floor(clamp(pixel.z / (1.0 / 200.0), 0.0, 199.0));" + - "pixel.y = mix(0.0, pixel.y, smoothstep(0.0, 0.1, pixel.z) * (1.0 - smoothstep(0.8, 1.0, pixel.z)));" + - "pixel.z = texture2D(curvesImage, vec2(1.0 / 200.0 * index, 0)).a;" + - "return pixel;" + - "}" + - "lowp vec3 applyRGBCurve(lowp vec3 pixel) {" + - "highp float index = floor(clamp(pixel.r / (1.0 / 200.0), 0.0, 199.0));" + - "pixel.r = texture2D(curvesImage, vec2(1.0 / 200.0 * index, 0)).r;" + - "index = floor(clamp(pixel.g / (1.0 / 200.0), 0.0, 199.0));" + - "pixel.g = clamp(texture2D(curvesImage, vec2(1.0 / 200.0 * index, 0)).g, 0.0, 1.0);" + - "index = floor(clamp(pixel.b / (1.0 / 200.0), 0.0, 199.0));" + - "pixel.b = clamp(texture2D(curvesImage, vec2(1.0 / 200.0 * index, 0)).b, 0.0, 1.0);" + - "return pixel;" + - "}" + - "highp vec3 fadeAdjust(highp vec3 color, highp float fadeVal) {" + - "return (color * (1.0 - fadeVal)) + ((color + (vec3(-0.9772) * pow(vec3(color), vec3(3.0)) + vec3(1.708) * pow(vec3(color), vec3(2.0)) + vec3(-0.1603) * vec3(color) + vec3(0.2878) - color * vec3(0.9))) * fadeVal);" + - "}" + - "lowp vec3 tintRaiseShadowsCurve(lowp vec3 color) {" + - "return vec3(-0.003671) * pow(color, vec3(3.0)) + vec3(0.3842) * pow(color, vec3(2.0)) + vec3(0.3764) * color + vec3(0.2515);" + - "}" + - "lowp vec3 tintShadows(lowp vec3 texel, lowp vec3 tintColor, lowp float tintAmount) {" + - "return clamp(mix(texel, mix(texel, tintRaiseShadowsCurve(texel), tintColor), tintAmount), 0.0, 1.0);" + - "} " + - "lowp vec3 tintHighlights(lowp vec3 texel, lowp vec3 tintColor, lowp float tintAmount) {" + - "return clamp(mix(texel, mix(texel, vec3(1.0) - tintRaiseShadowsCurve(vec3(1.0) - texel), (vec3(1.0) - tintColor)), tintAmount), 0.0, 1.0);" + - "}" + - "highp vec4 rnm(in highp vec2 tc) {" + - "highp float noise = sin(dot(tc, vec2(12.9898, 78.233))) * 43758.5453;" + - "return vec4(fract(noise), fract(noise * 1.2154), fract(noise * 1.3453), fract(noise * 1.3647)) * 2.0 - 1.0;" + - "}" + - "highp float fade(in highp float t) {" + - "return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);" + - "}" + - "highp float pnoise3D(in highp vec3 p) {" + - "highp vec3 pi = permTexUnit * floor(p) + permTexUnitHalf;" + - "highp vec3 pf = fract(p);" + - "highp float perm = rnm(pi.xy).a;" + - "highp float n000 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf);" + - "highp float n001 = dot(rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, pf - vec3(0.0, 0.0, 1.0));" + - "perm = rnm(pi.xy + vec2(0.0, permTexUnit)).a;" + - "highp float n010 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf - vec3(0.0, 1.0, 0.0));" + - "highp float n011 = dot(rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, pf - vec3(0.0, 1.0, 1.0));" + - "perm = rnm(pi.xy + vec2(permTexUnit, 0.0)).a;" + - "highp float n100 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf - vec3(1.0, 0.0, 0.0));" + - "highp float n101 = dot(rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, pf - vec3(1.0, 0.0, 1.0));" + - "perm = rnm(pi.xy + vec2(permTexUnit, permTexUnit)).a;" + - "highp float n110 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf - vec3(1.0, 1.0, 0.0));" + - "highp float n111 = dot(rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, pf - vec3(1.0, 1.0, 1.0));" + - "highp vec4 n_x = mix(vec4(n000, n001, n010, n011), vec4(n100, n101, n110, n111), fade(pf.x));" + - "highp vec2 n_xy = mix(n_x.xy, n_x.zw, fade(pf.y));" + - "return mix(n_xy.x, n_xy.y, fade(pf.z));" + - "}" + - "lowp vec2 coordRot(in lowp vec2 tc, in lowp float angle) {" + - "return vec2(((tc.x * 2.0 - 1.0) * cos(angle) - (tc.y * 2.0 - 1.0) * sin(angle)) * 0.5 + 0.5, ((tc.y * 2.0 - 1.0) * cos(angle) + (tc.x * 2.0 - 1.0) * sin(angle)) * 0.5 + 0.5);" + - "}" + - "void main() {" + - "lowp vec4 source = texture2D(sourceImage, texCoord);" + - "lowp vec4 result = source;" + - "const lowp float toolEpsilon = 0.005;" + - "if (skipTone < toolEpsilon) {" + - "result = vec4(applyRGBCurve(hslToRgb(applyLuminanceCurve(rgbToHsl(result.rgb)))), result.a);" + - "}" + - "mediump float hsLuminance = dot(result.rgb, hsLuminanceWeighting);" + - "mediump float shadow = clamp((pow(hsLuminance, 1.0 / shadows) + (-0.76) * pow(hsLuminance, 2.0 / shadows)) - hsLuminance, 0.0, 1.0);" + - "mediump float highlight = clamp((1.0 - (pow(1.0 - hsLuminance, 1.0 / (2.0 - highlights)) + (-0.8) * pow(1.0 - hsLuminance, 2.0 / (2.0 - highlights)))) - hsLuminance, -1.0, 0.0);" + - "lowp vec3 hsresult = vec3(0.0, 0.0, 0.0) + ((hsLuminance + shadow + highlight) - 0.0) * ((result.rgb - vec3(0.0, 0.0, 0.0)) / (hsLuminance - 0.0));" + - "mediump float contrastedLuminance = ((hsLuminance - 0.5) * 1.5) + 0.5;" + - "mediump float whiteInterp = contrastedLuminance * contrastedLuminance * contrastedLuminance;" + - "mediump float whiteTarget = clamp(highlights, 1.0, 2.0) - 1.0;" + - "hsresult = mix(hsresult, vec3(1.0), whiteInterp * whiteTarget);" + - "mediump float invContrastedLuminance = 1.0 - contrastedLuminance;" + - "mediump float blackInterp = invContrastedLuminance * invContrastedLuminance * invContrastedLuminance;" + - "mediump float blackTarget = 1.0 - clamp(shadows, 0.0, 1.0);" + - "hsresult = mix(hsresult, vec3(0.0), blackInterp * blackTarget);" + - "result = vec4(hsresult.rgb, result.a);" + - "result = vec4(clamp(((result.rgb - vec3(0.5)) * contrast + vec3(0.5)), 0.0, 1.0), result.a);" + - "if (abs(fadeAmount) > toolEpsilon) {" + - "result.rgb = fadeAdjust(result.rgb, fadeAmount);" + - "}" + - "lowp float satLuminance = dot(result.rgb, satLuminanceWeighting);" + - "lowp vec3 greyScaleColor = vec3(satLuminance);" + - "result = vec4(clamp(mix(greyScaleColor, result.rgb, saturation), 0.0, 1.0), result.a);" + - "if (abs(shadowsTintIntensity) > toolEpsilon) {" + - "result.rgb = tintShadows(result.rgb, shadowsTintColor, shadowsTintIntensity * 2.0);" + - "}" + - "if (abs(highlightsTintIntensity) > toolEpsilon) {" + - "result.rgb = tintHighlights(result.rgb, highlightsTintColor, highlightsTintIntensity * 2.0);" + - "}" + - "if (abs(exposure) > toolEpsilon) {" + - "mediump float mag = exposure * 1.045;" + - "mediump float exppower = 1.0 + abs(mag);" + - "if (mag < 0.0) {" + - "exppower = 1.0 / exppower;" + - "}" + - "result.r = 1.0 - pow((1.0 - result.r), exppower);" + - "result.g = 1.0 - pow((1.0 - result.g), exppower);" + - "result.b = 1.0 - pow((1.0 - result.b), exppower);" + - "}" + - "if (abs(warmth) > toolEpsilon) {" + - "highp vec3 yuvVec;" + - "if (warmth > 0.0 ) {" + - "yuvVec = vec3(0.1765, -0.1255, 0.0902);" + - "} else {" + - "yuvVec = -vec3(0.0588, 0.1569, -0.1255);" + - "}" + - "highp vec3 yuvColor = rgbToYuv(result.rgb);" + - "highp float luma = yuvColor.r;" + - "highp float curveScale = sin(luma * 3.14159);" + - "yuvColor += 0.375 * warmth * curveScale * yuvVec;" + - "result.rgb = yuvToRgb(yuvColor);" + - "}" + - "if (abs(grain) > toolEpsilon) {" + - "highp vec3 rotOffset = vec3(1.425, 3.892, 5.835);" + - "highp vec2 rotCoordsR = coordRot(texCoord, rotOffset.x);" + - "highp vec3 noise = vec3(pnoise3D(vec3(rotCoordsR * vec2(width / grainsize, height / grainsize),0.0)));" + - "lowp vec3 lumcoeff = vec3(0.299,0.587,0.114);" + - "lowp float luminance = dot(result.rgb, lumcoeff);" + - "lowp float lum = smoothstep(0.2, 0.0, luminance);" + - "lum += luminance;" + - "noise = mix(noise,vec3(0.0),pow(lum,4.0));" + - "result.rgb = result.rgb + noise * grain;" + - "}" + - "if (abs(vignette) > toolEpsilon) {" + - "const lowp float midpoint = 0.7;" + - "const lowp float fuzziness = 0.62;" + - "lowp float radDist = length(texCoord - 0.5) / sqrt(0.5);" + - "lowp float mag = easeInOutSigmoid(radDist * midpoint, fuzziness) * vignette * 0.645;" + - "result.rgb = mix(pow(result.rgb, vec3(1.0 / (1.0 - mag))), vec3(0.0), mag * mag);" + - "}" + - "gl_FragColor = result;" + - "}"; + "}" + + "}" + + "lowp vec3 applyLuminanceCurve(lowp vec3 pixel) {" + + "highp float index = floor(clamp(pixel.z / (1.0 / 200.0), 0.0, 199.0));" + + "pixel.y = mix(0.0, pixel.y, smoothstep(0.0, 0.1, pixel.z) * (1.0 - smoothstep(0.8, 1.0, pixel.z)));" + + "pixel.z = texture2D(curvesImage, vec2(1.0 / 200.0 * index, 0)).a;" + + "return pixel;" + + "}" + + "lowp vec3 applyRGBCurve(lowp vec3 pixel) {" + + "highp float index = floor(clamp(pixel.r / (1.0 / 200.0), 0.0, 199.0));" + + "pixel.r = texture2D(curvesImage, vec2(1.0 / 200.0 * index, 0)).r;" + + "index = floor(clamp(pixel.g / (1.0 / 200.0), 0.0, 199.0));" + + "pixel.g = clamp(texture2D(curvesImage, vec2(1.0 / 200.0 * index, 0)).g, 0.0, 1.0);" + + "index = floor(clamp(pixel.b / (1.0 / 200.0), 0.0, 199.0));" + + "pixel.b = clamp(texture2D(curvesImage, vec2(1.0 / 200.0 * index, 0)).b, 0.0, 1.0);" + + "return pixel;" + + "}" + + "highp vec3 fadeAdjust(highp vec3 color, highp float fadeVal) {" + + "return (color * (1.0 - fadeVal)) + ((color + (vec3(-0.9772) * pow(vec3(color), vec3(3.0)) + vec3(1.708) * pow(vec3(color), vec3(2.0)) + vec3(-0.1603) * vec3(color) + vec3(0.2878) - color * vec3(0.9))) * fadeVal);" + + "}" + + "lowp vec3 tintRaiseShadowsCurve(lowp vec3 color) {" + + "return vec3(-0.003671) * pow(color, vec3(3.0)) + vec3(0.3842) * pow(color, vec3(2.0)) + vec3(0.3764) * color + vec3(0.2515);" + + "}" + + "lowp vec3 tintShadows(lowp vec3 texel, lowp vec3 tintColor, lowp float tintAmount) {" + + "return clamp(mix(texel, mix(texel, tintRaiseShadowsCurve(texel), tintColor), tintAmount), 0.0, 1.0);" + + "} " + + "lowp vec3 tintHighlights(lowp vec3 texel, lowp vec3 tintColor, lowp float tintAmount) {" + + "return clamp(mix(texel, mix(texel, vec3(1.0) - tintRaiseShadowsCurve(vec3(1.0) - texel), (vec3(1.0) - tintColor)), tintAmount), 0.0, 1.0);" + + "}" + + "highp vec4 rnm(in highp vec2 tc) {" + + "highp float noise = sin(dot(tc, vec2(12.9898, 78.233))) * 43758.5453;" + + "return vec4(fract(noise), fract(noise * 1.2154), fract(noise * 1.3453), fract(noise * 1.3647)) * 2.0 - 1.0;" + + "}" + + "highp float fade(in highp float t) {" + + "return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);" + + "}" + + "highp float pnoise3D(in highp vec3 p) {" + + "highp vec3 pi = permTexUnit * floor(p) + permTexUnitHalf;" + + "highp vec3 pf = fract(p);" + + "highp float perm = rnm(pi.xy).a;" + + "highp float n000 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf);" + + "highp float n001 = dot(rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, pf - vec3(0.0, 0.0, 1.0));" + + "perm = rnm(pi.xy + vec2(0.0, permTexUnit)).a;" + + "highp float n010 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf - vec3(0.0, 1.0, 0.0));" + + "highp float n011 = dot(rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, pf - vec3(0.0, 1.0, 1.0));" + + "perm = rnm(pi.xy + vec2(permTexUnit, 0.0)).a;" + + "highp float n100 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf - vec3(1.0, 0.0, 0.0));" + + "highp float n101 = dot(rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, pf - vec3(1.0, 0.0, 1.0));" + + "perm = rnm(pi.xy + vec2(permTexUnit, permTexUnit)).a;" + + "highp float n110 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf - vec3(1.0, 1.0, 0.0));" + + "highp float n111 = dot(rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, pf - vec3(1.0, 1.0, 1.0));" + + "highp vec4 n_x = mix(vec4(n000, n001, n010, n011), vec4(n100, n101, n110, n111), fade(pf.x));" + + "highp vec2 n_xy = mix(n_x.xy, n_x.zw, fade(pf.y));" + + "return mix(n_xy.x, n_xy.y, fade(pf.z));" + + "}" + + "lowp vec2 coordRot(in lowp vec2 tc, in lowp float angle) {" + + "return vec2(((tc.x * 2.0 - 1.0) * cos(angle) - (tc.y * 2.0 - 1.0) * sin(angle)) * 0.5 + 0.5, ((tc.y * 2.0 - 1.0) * cos(angle) + (tc.x * 2.0 - 1.0) * sin(angle)) * 0.5 + 0.5);" + + "}" + + "void main() {" + + "lowp vec4 source = texture2D(sourceImage, texCoord);" + + "lowp vec4 result = source;" + + "const lowp float toolEpsilon = 0.005;" + + "if (skipTone < toolEpsilon) {" + + "result = vec4(applyRGBCurve(hslToRgb(applyLuminanceCurve(rgbToHsl(result.rgb)))), result.a);" + + "}" + + "mediump float hsLuminance = dot(result.rgb, hsLuminanceWeighting);" + + "mediump float shadow = clamp((pow(hsLuminance, 1.0 / shadows) + (-0.76) * pow(hsLuminance, 2.0 / shadows)) - hsLuminance, 0.0, 1.0);" + + "mediump float highlight = clamp((1.0 - (pow(1.0 - hsLuminance, 1.0 / (2.0 - highlights)) + (-0.8) * pow(1.0 - hsLuminance, 2.0 / (2.0 - highlights)))) - hsLuminance, -1.0, 0.0);" + + "lowp vec3 hsresult = vec3(0.0, 0.0, 0.0) + ((hsLuminance + shadow + highlight) - 0.0) * ((result.rgb - vec3(0.0, 0.0, 0.0)) / (hsLuminance - 0.0));" + + "mediump float contrastedLuminance = ((hsLuminance - 0.5) * 1.5) + 0.5;" + + "mediump float whiteInterp = contrastedLuminance * contrastedLuminance * contrastedLuminance;" + + "mediump float whiteTarget = clamp(highlights, 1.0, 2.0) - 1.0;" + + "hsresult = mix(hsresult, vec3(1.0), whiteInterp * whiteTarget);" + + "mediump float invContrastedLuminance = 1.0 - contrastedLuminance;" + + "mediump float blackInterp = invContrastedLuminance * invContrastedLuminance * invContrastedLuminance;" + + "mediump float blackTarget = 1.0 - clamp(shadows, 0.0, 1.0);" + + "hsresult = mix(hsresult, vec3(0.0), blackInterp * blackTarget);" + + "result = vec4(hsresult.rgb, result.a);" + + "result = vec4(clamp(((result.rgb - vec3(0.5)) * contrast + vec3(0.5)), 0.0, 1.0), result.a);" + + "if (abs(fadeAmount) > toolEpsilon) {" + + "result.rgb = fadeAdjust(result.rgb, fadeAmount);" + + "}" + + "lowp float satLuminance = dot(result.rgb, satLuminanceWeighting);" + + "lowp vec3 greyScaleColor = vec3(satLuminance);" + + "result = vec4(clamp(mix(greyScaleColor, result.rgb, saturation), 0.0, 1.0), result.a);" + + "if (abs(shadowsTintIntensity) > toolEpsilon) {" + + "result.rgb = tintShadows(result.rgb, shadowsTintColor, shadowsTintIntensity * 2.0);" + + "}" + + "if (abs(highlightsTintIntensity) > toolEpsilon) {" + + "result.rgb = tintHighlights(result.rgb, highlightsTintColor, highlightsTintIntensity * 2.0);" + + "}" + + "if (abs(exposure) > toolEpsilon) {" + + "mediump float mag = exposure * 1.045;" + + "mediump float exppower = 1.0 + abs(mag);" + + "if (mag < 0.0) {" + + "exppower = 1.0 / exppower;" + + "}" + + "result.r = 1.0 - pow((1.0 - result.r), exppower);" + + "result.g = 1.0 - pow((1.0 - result.g), exppower);" + + "result.b = 1.0 - pow((1.0 - result.b), exppower);" + + "}" + + "if (abs(warmth) > toolEpsilon) {" + + "highp vec3 yuvVec;" + + "if (warmth > 0.0 ) {" + + "yuvVec = vec3(0.1765, -0.1255, 0.0902);" + + "} else {" + + "yuvVec = -vec3(0.0588, 0.1569, -0.1255);" + + "}" + + "highp vec3 yuvColor = rgbToYuv(result.rgb);" + + "highp float luma = yuvColor.r;" + + "highp float curveScale = sin(luma * 3.14159);" + + "yuvColor += 0.375 * warmth * curveScale * yuvVec;" + + "result.rgb = yuvToRgb(yuvColor);" + + "}" + + "if (abs(grain) > toolEpsilon) {" + + "highp vec3 rotOffset = vec3(1.425, 3.892, 5.835);" + + "highp vec2 rotCoordsR = coordRot(texCoord, rotOffset.x);" + + "highp vec3 noise = vec3(pnoise3D(vec3(rotCoordsR * vec2(width / grainsize, height / grainsize),0.0)));" + + "lowp vec3 lumcoeff = vec3(0.299,0.587,0.114);" + + "lowp float luminance = dot(result.rgb, lumcoeff);" + + "lowp float lum = smoothstep(0.2, 0.0, luminance);" + + "lum += luminance;" + + "noise = mix(noise,vec3(0.0),pow(lum,4.0));" + + "result.rgb = result.rgb + noise * grain;" + + "}" + + "if (abs(vignette) > toolEpsilon) {" + + "const lowp float midpoint = 0.7;" + + "const lowp float fuzziness = 0.62;" + + "lowp float radDist = length(texCoord - 0.5) / sqrt(0.5);" + + "lowp float mag = easeInOutSigmoid(radDist * midpoint, fuzziness) * vignette * 0.645;" + + "result.rgb = mix(pow(result.rgb, vec3(1.0 / (1.0 - mag))), vec3(0.0), mag * mag);" + + "}" + + "gl_FragColor = result;" + + "}"; public interface FilterShadersDelegate { boolean shouldShowOriginal(); float getShadowsValue(); + float getSoftenSkinValue(); float getHighlightsValue(); float getEnhanceValue(); float getExposureValue(); @@ -515,12 +718,218 @@ public class FilterShaders { ByteBuffer fillAndGetCurveBuffer(); } - private boolean needUpdateBlurTexture = true; + private static class ToneCurve { - private int rgbToHsvShaderProgram; - private int rgbToHsvPositionHandle; - private int rgbToHsvInputTexCoordHandle; - private int rgbToHsvSourceImageHandle; + private float[] rgbCompositeCurve; + private float[] redCurve; + private float[] greenCurve; + private float[] blueCurve; + private int[] curveTexture = new int[1]; + + public ToneCurve() { + ArrayList defaultCurve = new ArrayList<>(); + defaultCurve.add(new PointF(0.0f, 0.0f)); + defaultCurve.add(new PointF(0.5f, 0.5f)); + defaultCurve.add(new PointF(1.0f, 1.0f)); + + ArrayList rgbDefaultCurve = new ArrayList<>(); + rgbDefaultCurve.add(new PointF(0.0f, 0.0f)); + rgbDefaultCurve.add(new PointF(0.47f, 0.57f)); + rgbDefaultCurve.add(new PointF(1.0f, 1.0f)); + + rgbCompositeCurve = getPreparedSplineCurve(rgbDefaultCurve); + redCurve = greenCurve = blueCurve = getPreparedSplineCurve(defaultCurve); + updateToneCurveTexture(); + } + + private float[] getPreparedSplineCurve(ArrayList points) { + for (int i = 0, N = points.size(); i < N; i++) { + PointF point = points.get(i); + point.x *= 255; + point.y *= 255; + } + + ArrayList splinePoints = splineCurve(points); + + PointF firstSplinePoint = splinePoints.get(0); + if (firstSplinePoint.x > 0) { + for (int i = (int) firstSplinePoint.x; i >= 0; i--) { + splinePoints.add(0, new PointF(i, 0)); + } + } + + PointF lastSplinePoint = splinePoints.get(splinePoints.size() - 1); + if (lastSplinePoint.x < 255) { + for (int i = (int) lastSplinePoint.x + 1; i <= 255; i++) { + splinePoints.add(new PointF(i, 255)); + } + } + + float[] preparedSplinePoints = new float[splinePoints.size()]; + for (int i = 0, N = splinePoints.size(); i < N; i++) { + PointF newPoint = splinePoints.get(i); + float distance = (float) Math.sqrt(Math.pow((newPoint.x - newPoint.y), 2.0)); + if (newPoint.x > newPoint.y) { + distance = -distance; + } + preparedSplinePoints[i] = distance; + } + + return preparedSplinePoints; + } + + private ArrayList splineCurve(ArrayList points) { + double[] sd = secondDerivative(points); + int n = sd.length; + if (n < 1) { + return null; + } + ArrayList output = new ArrayList<>(n + 1); + for (int i = 0; i < n - 1; i++) { + PointF cur = points.get(i); + PointF next = points.get(i + 1); + for (int x = (int) cur.x; x < (int) next.x; x++) { + double t = (double) (x - cur.x) / (next.x - cur.x); + double a = 1 - t; + double h = next.x - cur.x; + float y = (float) (a * cur.y + t * next.y + (h * h / 6) * ((a * a * a - a) * sd[i] + (t * t * t - t) * sd[i + 1])); + if (y > 255.0f) { + y = 255.0f; + } else if (y < 0.0f) { + y = 0.0f; + } + output.add(new PointF(x, y)); + } + } + output.add(points.get(points.size() - 1)); + return output; + } + + private double[] secondDerivative(ArrayList points) { + int n = points.size(); + if ((n <= 0) || (n == 1)) { + return null; + } + + double[][] matrix = new double[n][3]; + double[] result = new double[n]; + matrix[0][1] = 1; + matrix[0][0] = 0; + matrix[0][2] = 0; + + for (int i = 1; i < n - 1; i++) { + PointF P1 = points.get(i - 1); + PointF P2 = points.get(i); + PointF P3 = points.get(i + 1); + + matrix[i][0] = (double) (P2.x - P1.x) / 6; + matrix[i][1] = (double) (P3.x - P1.x) / 3; + matrix[i][2] = (double) (P3.x - P2.x) / 6; + result[i] = (double) (P3.y - P2.y) / (P3.x - P2.x) - (double) (P2.y - P1.y) / (P2.x - P1.x); + } + + result[0] = 0; + result[n - 1] = 0; + + matrix[n - 1][1] = 1; + matrix[n - 1][0] = 0; + matrix[n - 1][2] = 0; + + for (int i = 1; i < n; i++) { + double k = matrix[i][0] / matrix[i - 1][1]; + matrix[i][1] -= k * matrix[i - 1][2]; + matrix[i][0] = 0; + result[i] -= k * result[i - 1]; + } + + for (int i = n - 2; i >= 0; i--) { + double k = matrix[i][2] / matrix[i + 1][1]; + matrix[i][1] -= k * matrix[i + 1][0]; + matrix[i][2] = 0; + result[i] -= k * result[i + 1]; + } + + double[] output = new double[n]; + for (int i = 0; i < n; i++) { + output[i] = result[i] / matrix[i][1]; + } + return output; + } + + private void updateToneCurveTexture() { + GLES20.glGenTextures(1, curveTexture, 0); + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, curveTexture[0]); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); + + ByteBuffer curveBuffer = ByteBuffer.allocateDirect(256 * 4); + curveBuffer.order(ByteOrder.LITTLE_ENDIAN); + + if (redCurve.length >= 256 && greenCurve.length >= 256 && blueCurve.length >= 256 && rgbCompositeCurve.length >= 256) { + for (int currentCurveIndex = 0; currentCurveIndex < 256; currentCurveIndex++) { + int r = (int) Math.min(Math.max(currentCurveIndex + redCurve[currentCurveIndex], 0), 255); + int g = (int) Math.min(Math.max(currentCurveIndex + greenCurve[currentCurveIndex], 0), 255); + int b = (int) Math.min(Math.max(currentCurveIndex + blueCurve[currentCurveIndex], 0), 255); + + curveBuffer.put((byte) Math.min(Math.max(b + rgbCompositeCurve[b], 0), 255)); + curveBuffer.put((byte) Math.min(Math.max(g + rgbCompositeCurve[g], 0), 255)); + curveBuffer.put((byte) Math.min(Math.max(r + rgbCompositeCurve[r], 0), 255)); + curveBuffer.put((byte) 255); + } + curveBuffer.position(0); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, 256, 1, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, curveBuffer); + } + } + + public int getCurveTexture() { + return curveTexture[0]; + } + } + + private boolean needUpdateBlurTexture = true; + private boolean needUpdateSkinTexture = true; + + private boolean blurTextureCreated; + private boolean skinTextureCreated; + + private BlurProgram skinBlurProgram; + private float lastRadius; + private BlurProgram blurProgram; + + private boolean skinPassDrawn; + private int greenAndBlueChannelOverlayProgram; + private int greenAndBlueChannelOverlayPositionHandle; + private int greenAndBlueChannelOverlayInputTexCoordHandle; + private int greenAndBlueChannelOverlaySourceImageHandle; + private int greenAndBlueChannelOverlayMatrixHandle; + private ToneCurve toneCurve; + + private int highPassProgram; + private int highPassPositionHandle; + private int highPassInputTexCoordHandle; + private int highPassSourceImageHandle; + private int highPassInputImageHandle; + + private int compositeProgram; + private int compositePositionHandle; + private int compositeInputTexCoordHandle; + private int compositeSourceImageHandle; + private int compositeInputImageHandle; + private int compositeCurveImageHandle; + private int compositeMixtureHandle; + private int compositeMatrixHandle; + + private int boostProgram; + private int boostPositionHandle; + private int boostInputTexCoordHandle; + private int boostSourceImageHandle; + + private int[] rgbToHsvShaderProgram = new int[2]; + private int[] rgbToHsvPositionHandle = new int[2]; + private int[] rgbToHsvInputTexCoordHandle = new int[2]; + private int[] rgbToHsvSourceImageHandle = new int[2]; private int rgbToHsvMatrixHandle; private int enhanceShaderProgram; @@ -553,13 +962,6 @@ public class FilterShaders { private int shadowsTintColorHandle; private int highlightsTintColorHandle; - private int blurShaderProgram; - private int blurPositionHandle; - private int blurInputTexCoordHandle; - private int blurSourceImageHandle; - private int blurWidthHandle; - private int blurHeightHandle; - private int linearBlurShaderProgram; private int linearBlurPositionHandle; private int linearBlurInputTexCoordHandle; @@ -595,7 +997,8 @@ public class FilterShaders { private int[] enhanceTextures = new int[2]; private int[] enhanceFrameBuffer = new int[1]; - private int[] renderTexture = new int[3]; + private int[] renderTexture = new int[4]; + private int[] bitmapTextre = new int[1]; private int[] renderFrameBuffer; private int[] curveTextures = new int[1]; private boolean hsvGenerated; @@ -679,6 +1082,8 @@ public class FilterShaders { GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); + int[] linkStatus = new int[1]; + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexShaderCode); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, toolsFragmentShaderCode); @@ -690,7 +1095,6 @@ public class FilterShaders { GLES20.glBindAttribLocation(toolsShaderProgram, 1, "inputTexCoord"); GLES20.glLinkProgram(toolsShaderProgram); - int[] linkStatus = new int[1]; GLES20.glGetProgramiv(toolsShaderProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] == 0) { GLES20.glDeleteProgram(toolsShaderProgram); @@ -732,7 +1136,6 @@ public class FilterShaders { GLES20.glBindAttribLocation(sharpenShaderProgram, 1, "inputTexCoord"); GLES20.glLinkProgram(sharpenShaderProgram); - int[] linkStatus = new int[1]; GLES20.glGetProgramiv(sharpenShaderProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] == 0) { GLES20.glDeleteProgram(sharpenShaderProgram); @@ -749,33 +1152,14 @@ public class FilterShaders { return false; } - vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, blurVertexShaderCode); - fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, blurFragmentShaderCode); - - if (vertexShader != 0 && fragmentShader != 0) { - blurShaderProgram = GLES20.glCreateProgram(); - GLES20.glAttachShader(blurShaderProgram, vertexShader); - GLES20.glAttachShader(blurShaderProgram, fragmentShader); - GLES20.glBindAttribLocation(blurShaderProgram, 0, "position"); - GLES20.glBindAttribLocation(blurShaderProgram, 1, "inputTexCoord"); - - GLES20.glLinkProgram(blurShaderProgram); - int[] linkStatus = new int[1]; - GLES20.glGetProgramiv(blurShaderProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); - if (linkStatus[0] == 0) { - GLES20.glDeleteProgram(blurShaderProgram); - blurShaderProgram = 0; - } else { - blurPositionHandle = GLES20.glGetAttribLocation(blurShaderProgram, "position"); - blurInputTexCoordHandle = GLES20.glGetAttribLocation(blurShaderProgram, "inputTexCoord"); - blurSourceImageHandle = GLES20.glGetUniformLocation(blurShaderProgram, "sourceImage"); - blurWidthHandle = GLES20.glGetUniformLocation(blurShaderProgram, "texelWidthOffset"); - blurHeightHandle = GLES20.glGetUniformLocation(blurShaderProgram, "texelHeightOffset"); - } - } else { + blurProgram = new BlurProgram(8, 3.0f, false); + if (!blurProgram.create()) { return false; } + String extension = isVideo ? "#extension GL_OES_EGL_image_external : require" : ""; + String sampler2D = isVideo ? "samplerExternalOES" : "sampler2D"; + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexShaderCode); fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, linearBlurFragmentShaderCode); @@ -787,7 +1171,6 @@ public class FilterShaders { GLES20.glBindAttribLocation(linearBlurShaderProgram, 1, "inputTexCoord"); GLES20.glLinkProgram(linearBlurShaderProgram); - int[] linkStatus = new int[1]; GLES20.glGetProgramiv(linearBlurShaderProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] == 0) { GLES20.glDeleteProgram(linearBlurShaderProgram); @@ -818,7 +1201,6 @@ public class FilterShaders { GLES20.glBindAttribLocation(radialBlurShaderProgram, 1, "inputTexCoord"); GLES20.glLinkProgram(radialBlurShaderProgram); - int[] linkStatus = new int[1]; GLES20.glGetProgramiv(radialBlurShaderProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] == 0) { GLES20.glDeleteProgram(radialBlurShaderProgram); @@ -837,36 +1219,37 @@ public class FilterShaders { return false; } - if (isVideo) { - fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, rgbToHsvFragmentVideoShaderCode); - vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexVideoShaderCode); - } else { - fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, rgbToHsvFragmentShaderCode); - vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexShaderCode); - } - if (vertexShader != 0 && fragmentShader != 0) { - rgbToHsvShaderProgram = GLES20.glCreateProgram(); - GLES20.glAttachShader(rgbToHsvShaderProgram, vertexShader); - GLES20.glAttachShader(rgbToHsvShaderProgram, fragmentShader); - GLES20.glBindAttribLocation(rgbToHsvShaderProgram, 0, "position"); - GLES20.glBindAttribLocation(rgbToHsvShaderProgram, 1, "inputTexCoord"); - - GLES20.glLinkProgram(rgbToHsvShaderProgram); - int[] linkStatus = new int[1]; - GLES20.glGetProgramiv(rgbToHsvShaderProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); - if (linkStatus[0] == 0) { - GLES20.glDeleteProgram(rgbToHsvShaderProgram); - rgbToHsvShaderProgram = 0; + for (int a = 0; a < (isVideo ? 2 : 1); a++) { + if (a == 1 && isVideo) { + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexVideoShaderCode); + fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, String.format(Locale.US, rgbToHsvFragmentShaderCode, extension, sampler2D)); } else { - rgbToHsvPositionHandle = GLES20.glGetAttribLocation(rgbToHsvShaderProgram, "position"); - rgbToHsvInputTexCoordHandle = GLES20.glGetAttribLocation(rgbToHsvShaderProgram, "inputTexCoord"); - rgbToHsvSourceImageHandle = GLES20.glGetUniformLocation(rgbToHsvShaderProgram, "sourceImage"); - if (isVideo) { - rgbToHsvMatrixHandle = GLES20.glGetUniformLocation(rgbToHsvShaderProgram, "videoMatrix"); - } + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexShaderCode); + fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, String.format(Locale.US, rgbToHsvFragmentShaderCode, "", "sampler2D")); + } + if (vertexShader != 0 && fragmentShader != 0) { + rgbToHsvShaderProgram[a] = GLES20.glCreateProgram(); + GLES20.glAttachShader(rgbToHsvShaderProgram[a], vertexShader); + GLES20.glAttachShader(rgbToHsvShaderProgram[a], fragmentShader); + GLES20.glBindAttribLocation(rgbToHsvShaderProgram[a], 0, "position"); + GLES20.glBindAttribLocation(rgbToHsvShaderProgram[a], 1, "inputTexCoord"); + + GLES20.glLinkProgram(rgbToHsvShaderProgram[a]); + GLES20.glGetProgramiv(rgbToHsvShaderProgram[a], GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + GLES20.glDeleteProgram(rgbToHsvShaderProgram[a]); + rgbToHsvShaderProgram[a] = 0; + } else { + rgbToHsvPositionHandle[a] = GLES20.glGetAttribLocation(rgbToHsvShaderProgram[a], "position"); + rgbToHsvInputTexCoordHandle[a] = GLES20.glGetAttribLocation(rgbToHsvShaderProgram[a], "inputTexCoord"); + rgbToHsvSourceImageHandle[a] = GLES20.glGetUniformLocation(rgbToHsvShaderProgram[a], "sourceImage"); + if (a == 1) { + rgbToHsvMatrixHandle = GLES20.glGetUniformLocation(rgbToHsvShaderProgram[a], "videoMatrix"); + } + } + } else { + return false; } - } else { - return false; } vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexShaderCode); @@ -879,7 +1262,6 @@ public class FilterShaders { GLES20.glBindAttribLocation(enhanceShaderProgram, 1, "inputTexCoord"); GLES20.glLinkProgram(enhanceShaderProgram); - int[] linkStatus = new int[1]; GLES20.glGetProgramiv(enhanceShaderProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] == 0) { GLES20.glDeleteProgram(enhanceShaderProgram); @@ -895,6 +1277,118 @@ public class FilterShaders { return false; } + if (isVideo) { + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexVideoShaderCode); + } else { + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexShaderCode); + } + fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, String.format(Locale.US, greenAndBlueChannelOverlayFragmentShaderCode, extension, sampler2D)); + if (vertexShader != 0 && fragmentShader != 0) { + greenAndBlueChannelOverlayProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(greenAndBlueChannelOverlayProgram, vertexShader); + GLES20.glAttachShader(greenAndBlueChannelOverlayProgram, fragmentShader); + GLES20.glBindAttribLocation(greenAndBlueChannelOverlayProgram, 0, "position"); + GLES20.glBindAttribLocation(greenAndBlueChannelOverlayProgram, 1, "inputTexCoord"); + + GLES20.glLinkProgram(greenAndBlueChannelOverlayProgram); + GLES20.glGetProgramiv(greenAndBlueChannelOverlayProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + GLES20.glDeleteProgram(greenAndBlueChannelOverlayProgram); + greenAndBlueChannelOverlayProgram = 0; + } else { + greenAndBlueChannelOverlayPositionHandle = GLES20.glGetAttribLocation(greenAndBlueChannelOverlayProgram, "position"); + greenAndBlueChannelOverlayInputTexCoordHandle = GLES20.glGetAttribLocation(greenAndBlueChannelOverlayProgram, "inputTexCoord"); + greenAndBlueChannelOverlaySourceImageHandle = GLES20.glGetUniformLocation(greenAndBlueChannelOverlayProgram, "sourceImage"); + if (isVideo) { + greenAndBlueChannelOverlayMatrixHandle = GLES20.glGetUniformLocation(greenAndBlueChannelOverlayProgram, "videoMatrix"); + } + } + } else { + return false; + } + + fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, stillImageHighPassFilterFragmentShaderCode); + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleTwoTextureVertexShaderCode); + if (vertexShader != 0 && fragmentShader != 0) { + highPassProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(highPassProgram, vertexShader); + GLES20.glAttachShader(highPassProgram, fragmentShader); + GLES20.glBindAttribLocation(highPassProgram, 0, "position"); + GLES20.glBindAttribLocation(highPassProgram, 1, "inputTexCoord"); + + GLES20.glLinkProgram(highPassProgram); + GLES20.glGetProgramiv(highPassProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + GLES20.glDeleteProgram(highPassProgram); + highPassProgram = 0; + } else { + highPassPositionHandle = GLES20.glGetAttribLocation(highPassProgram, "position"); + highPassInputTexCoordHandle = GLES20.glGetAttribLocation(highPassProgram, "inputTexCoord"); + highPassSourceImageHandle = GLES20.glGetUniformLocation(highPassProgram, "sourceImage"); + highPassInputImageHandle = GLES20.glGetUniformLocation(highPassProgram, "inputImageTexture2"); + } + } else { + return false; + } + + fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, YUCIHighPassSkinSmoothingMaskBoostFilterFragmentShaderCode); + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleVertexShaderCode); + if (vertexShader != 0 && fragmentShader != 0) { + boostProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(boostProgram, vertexShader); + GLES20.glAttachShader(boostProgram, fragmentShader); + GLES20.glBindAttribLocation(boostProgram, 0, "position"); + GLES20.glBindAttribLocation(boostProgram, 1, "inputTexCoord"); + + GLES20.glLinkProgram(boostProgram); + GLES20.glGetProgramiv(boostProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + GLES20.glDeleteProgram(boostProgram); + boostProgram = 0; + } else { + boostPositionHandle = GLES20.glGetAttribLocation(boostProgram, "position"); + boostInputTexCoordHandle = GLES20.glGetAttribLocation(boostProgram, "inputTexCoord"); + boostSourceImageHandle = GLES20.glGetUniformLocation(boostProgram, "sourceImage"); + } + } else { + return false; + } + + if (isVideo) { + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleTwoTextureVertexVideoShaderCode); + } else { + vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, simpleTwoTextureVertexShaderCode); + } + fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, String.format(Locale.US, highpassSkinSmoothingCompositingFilterFragmentShaderString, extension, sampler2D)); + if (vertexShader != 0 && fragmentShader != 0) { + compositeProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(compositeProgram, vertexShader); + GLES20.glAttachShader(compositeProgram, fragmentShader); + GLES20.glBindAttribLocation(compositeProgram, 0, "position"); + GLES20.glBindAttribLocation(compositeProgram, 1, "inputTexCoord"); + + GLES20.glLinkProgram(compositeProgram); + GLES20.glGetProgramiv(compositeProgram, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + GLES20.glDeleteProgram(compositeProgram); + compositeProgram = 0; + } else { + compositePositionHandle = GLES20.glGetAttribLocation(compositeProgram, "position"); + compositeInputTexCoordHandle = GLES20.glGetAttribLocation(compositeProgram, "inputTexCoord"); + compositeSourceImageHandle = GLES20.glGetUniformLocation(compositeProgram, "sourceImage"); + compositeInputImageHandle = GLES20.glGetUniformLocation(compositeProgram, "inputImageTexture3"); + compositeCurveImageHandle = GLES20.glGetUniformLocation(compositeProgram, "toneCurveTexture"); + compositeMixtureHandle = GLES20.glGetUniformLocation(compositeProgram, "mixturePercent"); + if (isVideo) { + compositeMatrixHandle = GLES20.glGetUniformLocation(compositeProgram, "videoMatrix"); + } + } + } else { + return false; + } + + toneCurve = new ToneCurve(); + return true; } @@ -919,6 +1413,7 @@ public class FilterShaders { if (compileStatus[0] == 0) { if (BuildVars.LOGS_ENABLED) { FileLog.e(GLES20.glGetShaderInfoLog(shader)); + FileLog.e("shader code:\n " + shaderCode); } GLES20.glDeleteShader(shader); shader = 0; @@ -934,19 +1429,25 @@ public class FilterShaders { updateFrame = !hsvGenerated; } if (updateFrame) { - GLES20.glUseProgram(rgbToHsvShaderProgram); - GLES20.glActiveTexture(GLES20.GL_TEXTURE0); - if (isVideo) { + int index; + if (isVideo && !skinPassDrawn) { + GLES20.glUseProgram(rgbToHsvShaderProgram[1]); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTexture); GLES20.glUniformMatrix4fv(rgbToHsvMatrixHandle, 1, false, videoMatrix, 0); + index = 1; } else { - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[1]); + GLES20.glUseProgram(rgbToHsvShaderProgram[0]); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, skinPassDrawn ? renderTexture[1] : bitmapTextre[0]); + index = 0; } - GLES20.glUniform1i(rgbToHsvSourceImageHandle, 0); - GLES20.glEnableVertexAttribArray(rgbToHsvInputTexCoordHandle); - GLES20.glVertexAttribPointer(rgbToHsvInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); - GLES20.glEnableVertexAttribArray(rgbToHsvPositionHandle); - GLES20.glVertexAttribPointer(rgbToHsvPositionHandle, 2, GLES20.GL_FLOAT, false, 8, isVideo ? vertexInvertBuffer : vertexBuffer); + + GLES20.glUniform1i(rgbToHsvSourceImageHandle[index], 0); + GLES20.glEnableVertexAttribArray(rgbToHsvInputTexCoordHandle[index]); + GLES20.glVertexAttribPointer(rgbToHsvInputTexCoordHandle[index], 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(rgbToHsvPositionHandle[index]); + GLES20.glVertexAttribPointer(rgbToHsvPositionHandle[index], 2, GLES20.GL_FLOAT, false, 8, isVideo ? vertexInvertBuffer : vertexBuffer); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, enhanceFrameBuffer[0]); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, enhanceTextures[0], 0); @@ -1085,33 +1586,182 @@ public class FilterShaders { GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); } + public boolean drawSkinSmoothPass() { + if (delegate == null || delegate.shouldShowOriginal() || delegate.getSoftenSkinValue() <= 0.0f || renderBufferWidth <= 0 || renderBufferHeight <= 0) { + if (skinPassDrawn) { + hsvGenerated = false; + skinPassDrawn = false; + } + return false; + } + if (needUpdateSkinTexture || isVideo) { + float rad = renderBufferWidth * 0.006f; + if (skinBlurProgram == null || Math.abs(lastRadius - rad) > 0.0001) { + if (skinBlurProgram != null) { + skinBlurProgram.destroy(); + } + lastRadius = rad; + skinBlurProgram = new BlurProgram(lastRadius, 2.0f, true); + skinBlurProgram.create(); + } + + if (!skinTextureCreated) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[3]); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, renderBufferWidth, renderBufferHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); + skinTextureCreated = true; + } + + //green blue pass + GLES20.glUseProgram(greenAndBlueChannelOverlayProgram); + GLES20.glUniform1i(greenAndBlueChannelOverlaySourceImageHandle, 0); + GLES20.glEnableVertexAttribArray(greenAndBlueChannelOverlayInputTexCoordHandle); + GLES20.glVertexAttribPointer(greenAndBlueChannelOverlayInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(greenAndBlueChannelOverlayPositionHandle); + if (isVideo) { + GLES20.glUniformMatrix4fv(greenAndBlueChannelOverlayMatrixHandle, 1, false, videoMatrix, 0); + } + GLES20.glVertexAttribPointer(greenAndBlueChannelOverlayPositionHandle, 2, GLES20.GL_FLOAT, false, 8, vertexInvertBuffer); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, renderFrameBuffer[0]); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTexture[0], 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + if (isVideo) { + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTexture); + } else { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTextre[0]); + } + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + //blur pass + GLES20.glUseProgram(skinBlurProgram.blurShaderProgram); + GLES20.glUniform1i(skinBlurProgram.blurSourceImageHandle, 0); + GLES20.glEnableVertexAttribArray(skinBlurProgram.blurInputTexCoordHandle); + GLES20.glVertexAttribPointer(skinBlurProgram.blurInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(skinBlurProgram.blurPositionHandle); + GLES20.glVertexAttribPointer(skinBlurProgram.blurPositionHandle, 2, GLES20.GL_FLOAT, false, 8, vertexInvertBuffer); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, renderFrameBuffer[1]); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTexture[1], 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[0]); + GLES20.glUniform1f(skinBlurProgram.blurWidthHandle, 0.0f); + GLES20.glUniform1f(skinBlurProgram.blurHeightHandle, 1.0f / renderBufferHeight); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, renderFrameBuffer[3]); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTexture[3], 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[1]); + GLES20.glUniform1f(skinBlurProgram.blurWidthHandle, 1.0f / renderBufferWidth); + GLES20.glUniform1f(skinBlurProgram.blurHeightHandle, 0.0f); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + //high pass + GLES20.glUseProgram(highPassProgram); + GLES20.glUniform1i(highPassSourceImageHandle, 0); + GLES20.glUniform1i(highPassInputImageHandle, 1); + GLES20.glEnableVertexAttribArray(highPassInputTexCoordHandle); + GLES20.glVertexAttribPointer(highPassInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(highPassPositionHandle); + GLES20.glVertexAttribPointer(highPassPositionHandle, 2, GLES20.GL_FLOAT, false, 8, vertexInvertBuffer); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, renderFrameBuffer[1]); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTexture[1], 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[0]); + GLES20.glActiveTexture(GLES20.GL_TEXTURE1); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[3]); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + //boost pass + GLES20.glUseProgram(boostProgram); + GLES20.glUniform1i(boostSourceImageHandle, 0); + GLES20.glEnableVertexAttribArray(boostInputTexCoordHandle); + GLES20.glVertexAttribPointer(boostInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(boostPositionHandle); + GLES20.glVertexAttribPointer(boostPositionHandle, 2, GLES20.GL_FLOAT, false, 8, vertexInvertBuffer); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, renderFrameBuffer[3]); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTexture[3], 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[1]); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + needUpdateSkinTexture = false; + } + skinPassDrawn = true; + hsvGenerated = false; + GLES20.glUseProgram(compositeProgram); + GLES20.glUniform1i(compositeSourceImageHandle, 0); + GLES20.glUniform1i(compositeInputImageHandle, 1); + GLES20.glUniform1i(compositeCurveImageHandle, 2); + GLES20.glUniform1f(compositeMixtureHandle, delegate.getSoftenSkinValue()); + GLES20.glEnableVertexAttribArray(compositeInputTexCoordHandle); + GLES20.glVertexAttribPointer(compositeInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(compositePositionHandle); + GLES20.glVertexAttribPointer(compositePositionHandle, 2, GLES20.GL_FLOAT, false, 8, vertexInvertBuffer); + if (isVideo) { + GLES20.glUniformMatrix4fv(compositeMatrixHandle, 1, false, videoMatrix, 0); + } + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, renderFrameBuffer[1]); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTexture[1], 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + if (isVideo) { + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTexture); + } else { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTextre[0]); + } + GLES20.glActiveTexture(GLES20.GL_TEXTURE1); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[3]); + GLES20.glActiveTexture(GLES20.GL_TEXTURE2); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, toneCurve.getCurveTexture()); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + return true; + } + public boolean drawBlurPass() { int blurType = delegate != null ? delegate.getBlurType() : 0; if (isVideo || delegate == null || delegate.shouldShowOriginal() || blurType == 0) { return false; } if (needUpdateBlurTexture) { - GLES20.glUseProgram(blurShaderProgram); - GLES20.glUniform1i(blurSourceImageHandle, 0); - GLES20.glEnableVertexAttribArray(blurInputTexCoordHandle); - GLES20.glVertexAttribPointer(blurInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); - GLES20.glEnableVertexAttribArray(blurPositionHandle); - GLES20.glVertexAttribPointer(blurPositionHandle, 2, GLES20.GL_FLOAT, false, 8, vertexInvertBuffer); + if (!blurTextureCreated) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[2]); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, renderBufferWidth, renderBufferHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); + blurTextureCreated = true; + } + + GLES20.glUseProgram(blurProgram.blurShaderProgram); + GLES20.glUniform1i(blurProgram.blurSourceImageHandle, 0); + GLES20.glEnableVertexAttribArray(blurProgram.blurInputTexCoordHandle); + GLES20.glVertexAttribPointer(blurProgram.blurInputTexCoordHandle, 2, GLES20.GL_FLOAT, false, 8, textureBuffer); + GLES20.glEnableVertexAttribArray(blurProgram.blurPositionHandle); + GLES20.glVertexAttribPointer(blurProgram.blurPositionHandle, 2, GLES20.GL_FLOAT, false, 8, vertexInvertBuffer); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, renderFrameBuffer[0]); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTexture[0], 0); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[1]); - GLES20.glUniform1f(blurWidthHandle, 0.0f); - GLES20.glUniform1f(blurHeightHandle, 1.0f / renderBufferHeight); + GLES20.glUniform1f(blurProgram.blurWidthHandle, 0.0f); + GLES20.glUniform1f(blurProgram.blurHeightHandle, 1.0f / renderBufferHeight); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, renderFrameBuffer[2]); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, renderTexture[2], 0); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[0]); - GLES20.glUniform1f(blurWidthHandle, 1.0f / renderBufferWidth); - GLES20.glUniform1f(blurHeightHandle, 0.0f); + GLES20.glUniform1f(blurProgram.blurWidthHandle, 1.0f / renderBufferWidth); + GLES20.glUniform1f(blurProgram.blurHeightHandle, 0.0f); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); needUpdateBlurTexture = false; } @@ -1166,7 +1816,7 @@ public class FilterShaders { }*/ } - private Bitmap createBitmap(Bitmap bitmap, int orientation, int w, int h, float scale) { + private Bitmap createBitmap(Bitmap bitmap, int orientation, float scale) { Matrix matrix = new Matrix(); matrix.setScale(scale, scale); matrix.postRotate(orientation); @@ -1178,12 +1828,14 @@ public class FilterShaders { renderBufferHeight = h; if (renderFrameBuffer == null) { - renderFrameBuffer = new int[3]; - GLES20.glGenFramebuffers(3, renderFrameBuffer, 0); - GLES20.glGenTextures(3, renderTexture, 0); + renderFrameBuffer = new int[4]; + GLES20.glGenFramebuffers(4, renderFrameBuffer, 0); + GLES20.glGenTextures(4, renderTexture, 0); } if (bitmap != null && !bitmap.isRecycled()) { + GLES20.glGenTextures(1, bitmapTextre, 0); + float maxSize = AndroidUtilities.getPhotoSize(); if (renderBufferWidth > maxSize || renderBufferHeight > maxSize || orientation % 360 != 0) { float scale = 1; @@ -1207,37 +1859,25 @@ public class FilterShaders { renderBufferHeight = temp; } - bitmap = createBitmap(bitmap, orientation, renderBufferWidth, renderBufferHeight, scale); + bitmap = createBitmap(bitmap, orientation, scale); } - GLES20.glBindTexture(GL10.GL_TEXTURE_2D, renderTexture[1]); + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, bitmapTextre[0]); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0); - } else { - GLES20.glBindTexture(GL10.GL_TEXTURE_2D, renderTexture[1]); + } + + for (int a = 0; a < 2; a++) { + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, renderTexture[a]); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, renderBufferWidth, renderBufferHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); } - - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[0]); - GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); - GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); - GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); - GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); - GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, renderBufferWidth, renderBufferHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); - - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, renderTexture[2]); - GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); - GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); - GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); - GLES20.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); - GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, renderBufferWidth, renderBufferHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); } public FloatBuffer getTextureBuffer() { @@ -1273,6 +1913,7 @@ public class FilterShaders { public void requestUpdateBlurTexture() { needUpdateBlurTexture = true; + needUpdateSkinTexture = true; } public static FilterShadersDelegate getFilterShadersDelegate(MediaController.SavedFilterState lastState) { @@ -1282,6 +1923,11 @@ public class FilterShaders { return false; } + @Override + public float getSoftenSkinValue() { + return (lastState.softenSkinValue / 100.0f); + } + @Override public float getShadowsValue() { return (lastState.shadowsValue * 0.55f + 100.0f) / 100.0f; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/FireworksOverlay.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/FireworksOverlay.java index c49360363..091e8f794 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/FireworksOverlay.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/FireworksOverlay.java @@ -32,8 +32,8 @@ public class FireworksOverlay extends View { private float speedCoef = 1.0f; private int fallingDownCount; private static Drawable[] heartDrawable; - private static final int particlesCount = SharedConfig.getDevicePerfomanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW ? 50 : 60; - private static final int fallParticlesCount = SharedConfig.getDevicePerfomanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW ? 20 : 30; + private static final int particlesCount = SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW ? 50 : 60; + private static final int fallParticlesCount = SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW ? 20 : 30; private boolean isFebruary14; private static int[] colors = new int[] { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/FloatSeekBarAccessibilityDelegate.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/FloatSeekBarAccessibilityDelegate.java new file mode 100644 index 000000000..03bef1427 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/FloatSeekBarAccessibilityDelegate.java @@ -0,0 +1,78 @@ +package org.telegram.ui.Components; + +import android.os.Bundle; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.annotation.Nullable; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +public abstract class FloatSeekBarAccessibilityDelegate extends SeekBarAccessibilityDelegate { + + private final boolean setPercentsEnabled; + + public FloatSeekBarAccessibilityDelegate() { + this(false); + } + + public FloatSeekBarAccessibilityDelegate(boolean setPercentsEnabled) { + this.setPercentsEnabled = setPercentsEnabled; + } + + @Override + public void onInitializeAccessibilityNodeInfoInternal(@Nullable View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfoInternal(host, info); + if (setPercentsEnabled) { + final AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info); + infoCompat.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS); + infoCompat.setRangeInfo(AccessibilityNodeInfoCompat.RangeInfoCompat.obtain(AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_FLOAT, getMinValue(), getMaxValue(), getProgress())); + } + } + + @Override + public boolean performAccessibilityActionInternal(@Nullable View host, int action, Bundle args) { + if (super.performAccessibilityActionInternal(host, action, args)) { + return true; + } + if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SET_PROGRESS.getId()) { + setProgress(args.getFloat(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_PROGRESS_VALUE)); + return true; + } + return false; + } + + @Override + protected void doScroll(View host, boolean backward) { + float delta = getDelta(); + if (backward) { + delta *= -1f; + } + setProgress(Math.min(getMaxValue(), Math.max(getMinValue(), getProgress() + delta))); + } + + @Override + protected boolean canScrollBackward(View host) { + return getProgress() > getMinValue(); + } + + @Override + protected boolean canScrollForward(View host) { + return getProgress() < getMaxValue(); + } + + protected abstract float getProgress(); + + protected abstract void setProgress(float progress); + + protected float getMinValue() { + return 0f; + } + + protected float getMaxValue() { + return 1f; + } + + protected float getDelta() { + return 0.05f; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/FragmentContextView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/FragmentContextView.java index e3544294e..66c4a26d0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/FragmentContextView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/FragmentContextView.java @@ -18,6 +18,7 @@ import android.content.Intent; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Typeface; +import android.os.Build; import android.os.Bundle; import androidx.annotation.Keep; import android.text.SpannableStringBuilder; @@ -62,7 +63,9 @@ public class FragmentContextView extends FrameLayout implements NotificationCent private TextView titleTextView; private AnimatorSet animatorSet; private BaseFragment fragment; + private View applyingView; private FrameLayout frameLayout; + private View selector; private ImageView closeButton; private ImageView playbackSpeedButton; private FragmentContextView additionalContextView; @@ -77,6 +80,8 @@ public class FragmentContextView extends FrameLayout implements NotificationCent private boolean isLocation; + private FragmentContextViewDelegate delegate; + private boolean firstLocationsLoaded; private boolean loadingSharingCount; private int lastLocationSharingCount = -1; @@ -88,19 +93,33 @@ public class FragmentContextView extends FrameLayout implements NotificationCent } }; + public interface FragmentContextViewDelegate { + void onAnimation(boolean start, boolean show); + } + public FragmentContextView(Context context, BaseFragment parentFragment, boolean location) { + this(context, parentFragment, null, location); + } + + public FragmentContextView(Context context, BaseFragment parentFragment, View paddingView, boolean location) { super(context); fragment = parentFragment; + applyingView = paddingView; visible = true; isLocation = location; - ((ViewGroup) fragment.getFragmentView()).setClipToPadding(false); + if (applyingView == null) { + ((ViewGroup) fragment.getFragmentView()).setClipToPadding(false); + } setTag(1); frameLayout = new FrameLayout(context); frameLayout.setWillNotDraw(false); addView(frameLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 36, Gravity.TOP | Gravity.LEFT, 0, 0, 0, 0)); + selector = new View(context); + frameLayout.addView(selector, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + View shadow = new View(context); shadow.setBackgroundResource(R.drawable.header_shadow); addView(shadow, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.LEFT | Gravity.TOP, 0, 36, 0, 0)); @@ -108,6 +127,9 @@ public class FragmentContextView extends FrameLayout implements NotificationCent playButton = new ImageView(context); playButton.setScaleType(ImageView.ScaleType.CENTER); playButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_inappPlayerPlayPause), PorterDuff.Mode.SRC_IN)); + if (Build.VERSION.SDK_INT >= 21) { + playButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_inappPlayerPlayPause) & 0x19ffffff, 1, AndroidUtilities.dp(14))); + } addView(playButton, LayoutHelper.createFrame(36, 36, Gravity.TOP | Gravity.LEFT)); playButton.setOnClickListener(v -> { if (currentStyle == 0) { @@ -151,6 +173,9 @@ public class FragmentContextView extends FrameLayout implements NotificationCent closeButton = new ImageView(context); closeButton.setImageResource(R.drawable.miniplayer_close); closeButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_inappPlayerClose), PorterDuff.Mode.SRC_IN)); + if (Build.VERSION.SDK_INT >= 21) { + closeButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_inappPlayerClose) & 0x19ffffff, 1, AndroidUtilities.dp(14))); + } closeButton.setScaleType(ImageView.ScaleType.CENTER); addView(closeButton, LayoutHelper.createFrame(36, 36, Gravity.RIGHT | Gravity.TOP)); closeButton.setOnClickListener(v -> { @@ -256,15 +281,24 @@ public class FragmentContextView extends FrameLayout implements NotificationCent }); } + public void setDelegate(FragmentContextViewDelegate fragmentContextViewDelegate) { + delegate = fragmentContextViewDelegate; + } + private void updatePlaybackButton() { if (playbackSpeedButton == null) { return; } float currentPlaybackSpeed = MediaController.getInstance().getPlaybackSpeed(isMusic); + String key; if (currentPlaybackSpeed > 1) { - playbackSpeedButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_inappPlayerPlayPause), PorterDuff.Mode.SRC_IN)); + key = Theme.key_inappPlayerPlayPause; } else { - playbackSpeedButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_inappPlayerClose), PorterDuff.Mode.SRC_IN)); + key = Theme.key_inappPlayerClose; + } + playbackSpeedButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(key), PorterDuff.Mode.SRC_IN)); + if (Build.VERSION.SDK_INT >= 21) { + playbackSpeedButton.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(key) & 0x19ffffff, 1, AndroidUtilities.dp(14))); } } @@ -316,7 +350,7 @@ public class FragmentContextView extends FrameLayout implements NotificationCent public void setTopPadding(float value) { topPadding = value; if (fragment != null && getParent() != null) { - View view = fragment.getFragmentView(); + View view = applyingView != null ? applyingView : fragment.getFragmentView(); int additionalPadding = 0; if (additionalContextView != null && additionalContextView.getVisibility() == VISIBLE && additionalContextView.getParent() != null) { additionalPadding = AndroidUtilities.dp(36); @@ -336,6 +370,7 @@ public class FragmentContextView extends FrameLayout implements NotificationCent } currentStyle = style; if (style == 0 || style == 2) { + selector.setBackground(Theme.getSelectorDrawable(false)); frameLayout.setBackgroundColor(Theme.getColor(Theme.key_inappPlayerBackground)); frameLayout.setTag(Theme.key_inappPlayerBackground); titleTextView.setTextColor(Theme.getColor(Theme.key_inappPlayerTitle)); @@ -357,9 +392,10 @@ public class FragmentContextView extends FrameLayout implements NotificationCent closeButton.setContentDescription(LocaleController.getString("AccDescrStopLiveLocation", R.string.AccDescrStopLiveLocation)); } } else if (style == 1) { - titleTextView.setText(LocaleController.getString("ReturnToCall", R.string.ReturnToCall)); + selector.setBackground(null); frameLayout.setBackgroundColor(Theme.getColor(Theme.key_returnToCallBackground)); frameLayout.setTag(Theme.key_returnToCallBackground); + titleTextView.setText(LocaleController.getString("ReturnToCall", R.string.ReturnToCall)); titleTextView.setTextColor(Theme.getColor(Theme.key_returnToCallText)); titleTextView.setTag(Theme.key_returnToCallText); closeButton.setVisibility(GONE); @@ -664,11 +700,17 @@ public class FragmentContextView extends FrameLayout implements NotificationCent animatorSet = new AnimatorSet(); animatorSet.playTogether(ObjectAnimator.ofFloat(this, "topPadding", 0)); animatorSet.setDuration(200); + if (delegate != null) { + delegate.onAnimation(true, false); + } animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (animatorSet != null && animatorSet.equals(animation)) { setVisibility(GONE); + if (delegate != null) { + delegate.onAnimation(false, false); + } animatorSet = null; } } @@ -687,6 +729,10 @@ public class FragmentContextView extends FrameLayout implements NotificationCent ((LayoutParams) getLayoutParams()).topMargin = -AndroidUtilities.dp(36); } yPosition = 0; + if (delegate != null) { + delegate.onAnimation(true, true); + delegate.onAnimation(false, true); + } } if (!visible) { if (!create) { @@ -700,12 +746,18 @@ public class FragmentContextView extends FrameLayout implements NotificationCent } else { ((LayoutParams) getLayoutParams()).topMargin = -AndroidUtilities.dp(36); } + if (delegate != null) { + delegate.onAnimation(true, true); + } animatorSet.playTogether(ObjectAnimator.ofFloat(this, "topPadding", AndroidUtilities.dp2(36))); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (animatorSet != null && animatorSet.equals(animation)) { + if (delegate != null) { + delegate.onAnimation(false, true); + } animatorSet = null; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/GestureDetector2.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/GestureDetector2.java index b9461a011..13690d4ba 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/GestureDetector2.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/GestureDetector2.java @@ -302,7 +302,7 @@ public class GestureDetector2 { final float scrollX = mLastFocusX - focusX; final float scrollY = mLastFocusY - focusY; if (mIsDoubleTapping) { - handled |= mDoubleTapListener.onDoubleTapEvent(ev); + handled |= mDoubleTapListener != null && mDoubleTapListener.onDoubleTapEvent(ev); } else if (mAlwaysInTapRegion) { final int deltaX = (int) (focusX - mDownFocusX); final int deltaY = (int) (focusY - mDownFocusY); @@ -353,7 +353,7 @@ public class GestureDetector2 { mListener.onUp(ev); MotionEvent currentUpEvent = MotionEvent.obtain(ev); if (mIsDoubleTapping) { - handled |= mDoubleTapListener.onDoubleTapEvent(ev); + handled |= mDoubleTapListener != null && mDoubleTapListener.onDoubleTapEvent(ev); } else if (mInLongPress) { mHandler.removeMessages(TAP); mInLongPress = false; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/GroupedPhotosListView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/GroupedPhotosListView.java index 6550852fa..7498b756e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/GroupedPhotosListView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/GroupedPhotosListView.java @@ -44,6 +44,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes private int animateToDX; private int animateToDXStart; private int animateToItem = -1; + private boolean animateToItemFast; private android.widget.Scroller scroll; private GestureDetector gestureDetector; private boolean scrolling; @@ -51,12 +52,14 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes private boolean ignoreChanges; private boolean animationsEnabled = true; private int nextPhotoScrolling = -1; + private boolean hasPhotos; + private boolean animateBackground = true; private GroupedPhotosListViewDelegate delegate; private ValueAnimator showAnimator; private ValueAnimator hideAnimator; - private float drawAlpha = 0f; + private float drawAlpha; public interface GroupedPhotosListViewDelegate { int getCurrentIndex(); @@ -68,6 +71,8 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes ArrayList getPageBlockArr(); Object getParentObject(); void setCurrentIndex(int index); + void onShowAnimationStart(); + void onStopScrolling(); } public GroupedPhotosListView(Context context) { @@ -104,6 +109,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes int slideshowMessageId = delegate.getSlideshowMessageId(); int currentAccount = delegate.getCurrentAccount(); + hasPhotos = false; boolean changed = false; int newCount = 0; Object currentObject = null; @@ -111,6 +117,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes ImageLocation location = imagesArrLocations.get(currentIndex); newCount = imagesArrLocations.size(); currentObject = location; + hasPhotos = true; } else if (imagesArr != null && !imagesArr.isEmpty()) { MessageObject messageObject = imagesArr.get(currentIndex); currentObject = messageObject; @@ -119,6 +126,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes currentGroupId = messageObject.getGroupIdForUse(); } if (currentGroupId != 0) { + hasPhotos = true; int max = Math.min(currentIndex + 10, imagesArr.size()); for (int a = currentIndex; a < max; a++) { MessageObject object = imagesArr.get(a); @@ -146,6 +154,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes currentGroupId = pageBlock.groupId; } if (currentGroupId != 0) { + hasPhotos = true; for (int a = currentIndex, size = pageBlockArr.size(); a < size; a++) { TLRPC.PageBlock object = pageBlockArr.get(a); if (object.groupId == currentGroupId) { @@ -168,9 +177,10 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes return; } if (animationsEnabled) { - if (newCount <= 1) { + if (!hasPhotos) { if (showAnimator != null) { showAnimator.cancel(); + showAnimator = null; } if (drawAlpha > 0f && currentPhotos.size() > 1) { if (hideAnimator == null) { @@ -203,6 +213,13 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes showAnimator = ValueAnimator.ofFloat(drawAlpha, 1f); showAnimator.setDuration((long) (200 * (1f - drawAlpha))); showAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (delegate != null) { + delegate.onShowAnimationStart(); + } + } + @Override public void onAnimationEnd(Animator animation) { if (showAnimator == animation) { @@ -214,7 +231,6 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes drawAlpha = (float) a.getAnimatedValue(); invalidate(); }); - showAnimator.start(); } } } @@ -224,7 +240,12 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes } else { int newImageIndex = currentObjects.indexOf(currentObject); if (currentImage != newImageIndex && newImageIndex != -1) { - if (animateAllLine) { + boolean animate = animateAllLine; + if (!animate && !moving && (newImageIndex == currentImage - 1 || newImageIndex == currentImage + 1)) { + animate = true; + animateToItemFast = true; + } + if (animate) { nextImage = animateToItem = newImageIndex; animateToDX = (currentImage - newImageIndex) * (itemWidth + itemSpacing); moving = true; @@ -250,6 +271,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes currentPhotos.addAll(imagesArrLocations); currentImage = currentIndex; animateToItem = -1; + animateToItemFast = false; } else if (imagesArr != null && !imagesArr.isEmpty()) { if (currentGroupId != 0 || slideshowMessageId != 0) { int max = Math.min(currentIndex + 10, imagesArr.size()); @@ -264,6 +286,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes } currentImage = 0; animateToItem = -1; + animateToItemFast = false; int min = Math.max(currentIndex - 10, 0); for (int a = currentIndex - 1; a >= min; a--) { MessageObject object = imagesArr.get(a); @@ -289,6 +312,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes } currentImage = 0; animateToItem = -1; + animateToItemFast = false; for (int a = currentIndex - 1; a >= 0; a--) { TLRPC.PageBlock object = pageBlockArr.get(a); if (object.groupId == currentGroupId) { @@ -434,6 +458,9 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes } } } + if (showAnimator != null && !showAnimator.isStarted()) { + showAnimator.start(); + } } @Override @@ -442,6 +469,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes scroll.abortAnimation(); } animateToItem = -1; + animateToItemFast = false; return true; } @@ -580,12 +608,15 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes } if (nextPhotoScrolling >= 0 && nextPhotoScrolling < currentObjects.size()) { stopedScrolling = true; - + animateToItemFast = false; nextImage = animateToItem = nextPhotoScrolling; animateToDX = (currentImage - nextPhotoScrolling) * (itemWidth + itemSpacing); animateToDXStart = drawDx; moveLineProgress = 1.0f; nextPhotoScrolling = -1; + if (delegate != null) { + delegate.onStopScrolling(); + } } invalidate(); } @@ -618,11 +649,18 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes @Override protected void onDraw(Canvas canvas) { + if (!hasPhotos && imagesToDraw.isEmpty()) { + return; + } + float bgAlpha = drawAlpha; + if (!animateBackground) { + bgAlpha = hasPhotos ? 1f : 0f; + } + backgroundPaint.setAlpha((int) (0x7F * bgAlpha)); + canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), backgroundPaint); if (imagesToDraw.isEmpty()) { return; } - backgroundPaint.setAlpha((int) (0x7F * drawAlpha)); - canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), backgroundPaint); int count = imagesToDraw.size(); int moveX = drawDx; @@ -707,7 +745,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes lastUpdateTime = newTime; if (animateToItem >= 0) { if (moveLineProgress > 0.0f) { - moveLineProgress -= dt / 200.0f; + moveLineProgress -= dt / (animateToItemFast ? 100.0f : 200.0f); if (animateToItem == currentImage) { if (currentItemProgress < 1.0f) { currentItemProgress += dt / 200.0f; @@ -740,6 +778,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes stopedScrolling = false; drawDx = 0; animateToItem = -1; + animateToItemFast = false; } } fillImages(true, drawDx); @@ -769,7 +808,7 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes } public boolean hasPhotos() { - return !currentPhotos.isEmpty() && hideAnimator == null; + return hasPhotos && hideAnimator == null && (drawAlpha > 0f || !animateBackground || (showAnimator != null && showAnimator.isStarted())); } public boolean isAnimationsEnabled() { @@ -788,13 +827,18 @@ public class GroupedPhotosListView extends View implements GestureDetector.OnGes hideAnimator.cancel(); hideAnimator = null; } - drawAlpha = 1f; + drawAlpha = 0f; invalidate(); } } } + public void setAnimateBackground(boolean animateBackground) { + this.animateBackground = animateBackground; + } + public void reset() { + hasPhotos = false; if (animationsEnabled) { drawAlpha = 0f; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ImageUpdater.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ImageUpdater.java index f2a636447..701bb8066 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ImageUpdater.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ImageUpdater.java @@ -10,6 +10,7 @@ package org.telegram.ui.Components; import android.Manifest; import android.app.Activity; +import android.app.Dialog; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -20,6 +21,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; +import android.view.View; import androidx.core.content.FileProvider; import androidx.exifinterface.media.ExifInterface; @@ -33,10 +35,12 @@ import org.telegram.messenger.ImageLocation; import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; +import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.SendMessagesHelper; +import org.telegram.messenger.SharedConfig; import org.telegram.messenger.UserConfig; import org.telegram.messenger.Utilities; import org.telegram.messenger.VideoEditedInfo; @@ -50,7 +54,7 @@ import org.telegram.ui.PhotoAlbumPickerActivity; import org.telegram.ui.PhotoCropActivity; import org.telegram.ui.PhotoPickerActivity; import org.telegram.ui.PhotoViewer; -import org.telegram.ui.SettingsActivity; +import org.telegram.ui.ProfileActivity; import java.io.File; import java.util.ArrayList; @@ -62,49 +66,94 @@ import tw.nekomimi.nekogram.BottomBuilder; public class ImageUpdater implements NotificationCenter.NotificationCenterDelegate, PhotoCropActivity.PhotoEditActivityDelegate { public BaseFragment parentFragment; - public ImageUpdaterDelegate delegate; + private ImageUpdaterDelegate delegate; + private ChatAttachAlert chatAttachAlert; private int currentAccount = UserConfig.selectedAccount; private ImageReceiver imageReceiver; public String currentPicturePath; private TLRPC.PhotoSize bigPhoto; private TLRPC.PhotoSize smallPhoto; - public String uploadingImage; + private String uploadingImage; + private String uploadingVideo; + private String videoPath; + private MessageObject convertingVideo; private File picturePath = null; private String finalPath; private boolean clearAfterUpdate; + private boolean useAttachMenu = true; + private boolean openWithFrontfaceCamera; private boolean searchAvailable = true; private boolean uploadAfterSelect = true; + private TLRPC.InputFile uploadedPhoto; + private TLRPC.InputFile uploadedVideo; + private double videoTimestamp; + + private boolean canSelectVideo; + + private final static int attach_photo = 0; + public interface ImageUpdaterDelegate { - void didUploadPhoto(TLRPC.InputFile file, TLRPC.PhotoSize bigSize, TLRPC.PhotoSize smallSize); + void didUploadPhoto(TLRPC.InputFile photo, TLRPC.InputFile video, double videoStartTimestamp, String videoPath, TLRPC.PhotoSize bigSize, TLRPC.PhotoSize smallSize); default String getInitialSearchString() { return null; } + + default void onUploadProgressChanged(float progress) { + + } + + default void didStartUpload(boolean isVideo) { + + } + } + + public boolean isUploadingImage() { + return uploadingImage != null || uploadingVideo != null || convertingVideo != null; } public void clear() { - if (uploadingImage != null) { + if (uploadingImage != null || uploadingVideo != null || convertingVideo != null) { clearAfterUpdate = true; } else { parentFragment = null; delegate = null; } + if (chatAttachAlert != null) { + chatAttachAlert.dismissInternal(); + chatAttachAlert.onDestroy(); + } } - public ImageUpdater() { + public void setOpenWithFrontfaceCamera(boolean value) { + openWithFrontfaceCamera = value; + } + + public ImageUpdater(boolean allowVideo) { imageReceiver = new ImageReceiver(null); + canSelectVideo = allowVideo && Build.VERSION.SDK_INT > 18; + } + + public void setDelegate(ImageUpdaterDelegate imageUpdaterDelegate) { + delegate = imageUpdaterDelegate; } public void openMenu(boolean hasAvatar, Runnable onDeleteAvatar) { if (parentFragment == null || parentFragment.getParentActivity() == null) { return; } + + if (useAttachMenu) { + openAttachMenu(); + return; + } + BottomBuilder builder = new BottomBuilder(parentFragment.getParentActivity()); - if (hasAvatar && parentFragment instanceof SettingsActivity) { + if (hasAvatar && parentFragment instanceof ProfileActivity) { builder.addItem(LocaleController.getString("Open", R.string.Open), R.drawable.baseline_visibility_24, __ -> { @@ -114,7 +163,7 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega if (user.photo.dc_id != 0) { user.photo.photo_big.dc_id = user.photo.dc_id; } - PhotoViewer.getInstance().openPhoto(user.photo.photo_big, ((SettingsActivity) parentFragment).provider); + PhotoViewer.getInstance().openPhoto(user.photo.photo_big, ((ProfileActivity) parentFragment).provider); } return Unit.INSTANCE; @@ -145,6 +194,15 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega } + if (canSelectVideo) { + + builder.addItem(LocaleController.getString("ChooseRecordVideo", R.string.ChooseRecordVideo), R.drawable.baseline_videocam_16, __ -> { + openVideoCamera(); + return Unit.INSTANCE; + }); + + } + if (hasAvatar) { builder.addItem(LocaleController.getString("DeletePhoto", R.string.DeletePhoto), R.drawable.baseline_delete_24, true, __ -> { @@ -156,17 +214,42 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega BottomSheet sheet = builder.create(); parentFragment.showDialog(sheet); - sheet.setItemColor(searchAvailable ? 3 : 2, Theme.getColor(Theme.key_dialogTextRed2), Theme.getColor(Theme.key_dialogRedIcon)); } public void setSearchAvailable(boolean value) { - searchAvailable = value; + useAttachMenu = searchAvailable = value; } public void setUploadAfterSelect(boolean value) { uploadAfterSelect = value; } + public void onResume() { + if (chatAttachAlert != null) { + chatAttachAlert.onResume(); + } + } + + public void onPause() { + if (chatAttachAlert != null) { + chatAttachAlert.onPause(); + } + } + + public boolean dismissDialogOnPause(Dialog dialog) { + return dialog != chatAttachAlert; + } + + public boolean dismissCurrentDialog(Dialog dialog) { + if (chatAttachAlert != null && dialog == chatAttachAlert) { + chatAttachAlert.getPhotoLayout().closeCamera(false); + chatAttachAlert.dismissInternal(); + chatAttachAlert.getPhotoLayout().hideCamera(true); + return true; + } + return false; + } + public void openSearch() { if (parentFragment == null) { return; @@ -206,6 +289,7 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega } else { info.searchImage = searchImage; } + info.videoEditedInfo = searchImage.editedInfo; info.thumbPath = searchImage.thumbPath; info.caption = searchImage.caption != null ? searchImage.caption.toString() : null; info.entities = searchImage.entities; @@ -221,15 +305,151 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega } }); + fragment.setMaxSelectedPhotos(1, false); fragment.setInitialSearchString(delegate.getInitialSearchString()); parentFragment.presentFragment(fragment); } + private void openAttachMenu() { + if (parentFragment == null || parentFragment.getParentActivity() == null) { + return; + } + createChatAttachView(); + chatAttachAlert.setOpenWithFrontFaceCamera(openWithFrontfaceCamera); + chatAttachAlert.setMaxSelectedPhotos(1, false); + chatAttachAlert.getPhotoLayout().loadGalleryPhotos(); + if (Build.VERSION.SDK_INT == 21 || Build.VERSION.SDK_INT == 22) { + AndroidUtilities.hideKeyboard(parentFragment.getFragmentView().findFocus()); + } + chatAttachAlert.init(); + parentFragment.showDialog(chatAttachAlert); + } + + private void createChatAttachView() { + if (parentFragment == null || parentFragment.getParentActivity() == null) { + return; + } + if (chatAttachAlert == null) { + chatAttachAlert = new ChatAttachAlert(parentFragment.getParentActivity(), parentFragment); + chatAttachAlert.setAvatarPicker(canSelectVideo ? 2 : 1, searchAvailable); + chatAttachAlert.setDelegate(new ChatAttachAlert.ChatAttachViewDelegate() { + + @Override + public void didPressedButton(int button, boolean arg, boolean notify, int scheduleDate) { + if (parentFragment == null || parentFragment.getParentActivity() == null || chatAttachAlert == null) { + return; + } + if (button == 8 || button == 7) { + if (button != 8) { + chatAttachAlert.dismiss(); + } + HashMap photos = chatAttachAlert.getPhotoLayout().getSelectedPhotos(); + ArrayList order = chatAttachAlert.getPhotoLayout().getSelectedPhotosOrder(); + + ArrayList media = new ArrayList<>(); + for (int a = 0; a < order.size(); a++) { + Object object = photos.get(order.get(a)); + SendMessagesHelper.SendingMediaInfo info = new SendMessagesHelper.SendingMediaInfo(); + media.add(info); + if (object instanceof MediaController.PhotoEntry) { + MediaController.PhotoEntry photoEntry = (MediaController.PhotoEntry) object; + if (photoEntry.imagePath != null) { + info.path = photoEntry.imagePath; + } else { + info.path = photoEntry.path; + } + info.thumbPath = photoEntry.thumbPath; + info.videoEditedInfo = photoEntry.editedInfo; + info.isVideo = photoEntry.isVideo; + info.caption = photoEntry.caption != null ? photoEntry.caption.toString() : null; + info.entities = photoEntry.entities; + info.masks = photoEntry.stickers; + info.ttl = photoEntry.ttl; + } else if (object instanceof MediaController.SearchImage) { + MediaController.SearchImage searchImage = (MediaController.SearchImage) object; + if (searchImage.imagePath != null) { + info.path = searchImage.imagePath; + } else { + info.searchImage = searchImage; + } + info.thumbPath = searchImage.thumbPath; + info.videoEditedInfo = searchImage.editedInfo; + info.caption = searchImage.caption != null ? searchImage.caption.toString() : null; + info.entities = searchImage.entities; + info.masks = searchImage.stickers; + info.ttl = searchImage.ttl; + if (searchImage.inlineResult != null && searchImage.type == 1) { + info.inlineResult = searchImage.inlineResult; + info.params = searchImage.params; + } + + searchImage.date = (int) (System.currentTimeMillis() / 1000); + } + } + didSelectPhotos(media); + return; + } else if (chatAttachAlert != null) { + chatAttachAlert.dismissWithButtonClick(button); + } + processSelectedAttach(button); + } + + @Override + public View getRevealView() { + return null; + } + + @Override + public void didSelectBot(TLRPC.User user) { + + } + + @Override + public void onCameraOpened() { + AndroidUtilities.hideKeyboard(parentFragment.getFragmentView().findFocus()); + } + + @Override + public void needEnterComment() { + + } + + @Override + public void doOnIdle(Runnable runnable) { + runnable.run(); + } + + private void processSelectedAttach(int which) { + if (which == attach_photo) { + openCamera(); + } + } + + @Override + public void openAvatarsSearch() { + openSearch(); + } + }); + } + } + private void didSelectPhotos(ArrayList photos) { if (!photos.isEmpty()) { SendMessagesHelper.SendingMediaInfo info = photos.get(0); Bitmap bitmap = null; - if (info.path != null) { + MessageObject avatarObject = null; + if (info.isVideo || info.videoEditedInfo != null) { + TLRPC.TL_message message = new TLRPC.TL_message(); + message.id = 0; + message.message = ""; + message.media = new TLRPC.TL_messageMediaEmpty(); + message.action = new TLRPC.TL_messageActionEmpty(); + message.dialog_id = 0; + avatarObject = new MessageObject(UserConfig.selectedAccount, message, false); + avatarObject.messageOwner.attachPath = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_avatar.mp4").getAbsolutePath(); + avatarObject.videoEditedInfo = info.videoEditedInfo; + bitmap = ImageLoader.loadBitmap(info.thumbPath, null, 800, 800, true); + } else if (info.path != null) { bitmap = ImageLoader.loadBitmap(info.path, null, 800, 800, true); } else if (info.searchImage != null) { if (info.searchImage.photo != null) { @@ -268,7 +488,7 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega bitmap = null; } } - processBitmap(bitmap); + processBitmap(bitmap, avatarObject); } } @@ -278,7 +498,7 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega } try { if (Build.VERSION.SDK_INT >= 23 && parentFragment.getParentActivity().checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - parentFragment.getParentActivity().requestPermissions(new String[]{Manifest.permission.CAMERA}, 19); + parentFragment.getParentActivity().requestPermissions(new String[]{Manifest.permission.CAMERA}, 20); return; } Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); @@ -299,6 +519,43 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega } } + public void openVideoCamera() { + if (parentFragment == null || parentFragment.getParentActivity() == null) { + return; + } + try { + if (Build.VERSION.SDK_INT >= 23 && parentFragment.getParentActivity().checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + parentFragment.getParentActivity().requestPermissions(new String[]{Manifest.permission.CAMERA}, 19); + return; + } + Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + File video = AndroidUtilities.generateVideoPath(); + if (video != null) { + if (Build.VERSION.SDK_INT >= 24) { + takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(parentFragment.getParentActivity(), BuildConfig.APPLICATION_ID + ".provider", video)); + takeVideoIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + takeVideoIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } else if (Build.VERSION.SDK_INT >= 18) { + takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(video)); + } + takeVideoIntent.putExtra("android.intent.extras.CAMERA_FACING", 1); + takeVideoIntent.putExtra("android.intent.extras.LENS_FACING_FRONT", 1); + takeVideoIntent.putExtra("android.intent.extra.USE_FRONT_CAMERA", true); + takeVideoIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10); + currentPicturePath = video.getAbsolutePath(); + } + parentFragment.startActivityForResult(takeVideoIntent, 15); + } catch (Exception e) { + FileLog.e(e); + } + } + + public void onRequestPermissionsResultFragment(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == 17 && chatAttachAlert != null) { + chatAttachAlert.getPhotoLayout().checkCamera(false); + } + } + public void openGallery() { if (parentFragment == null) { return; @@ -309,7 +566,7 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega return; } } - PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(1, false, false, null); + PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(canSelectVideo ? PhotoAlbumPickerActivity.SELECT_TYPE_AVATAR_VIDEO : PhotoAlbumPickerActivity.SELECT_TYPE_AVATAR, false, false, null); fragment.setAllowSearchImages(searchAvailable); fragment.setDelegate(new PhotoAlbumPickerActivity.PhotoAlbumPickerActivityDelegate() { @Override @@ -349,13 +606,67 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega } catch (Exception e) { FileLog.e(e); Bitmap bitmap = ImageLoader.loadBitmap(path, uri, 800, 800, true); - processBitmap(bitmap); + processBitmap(bitmap, null); } } + public void openPhotoForEdit(String path, String thumb, int orientation, boolean isVideo) { + final ArrayList arrayList = new ArrayList<>(); + MediaController.PhotoEntry photoEntry = new MediaController.PhotoEntry(0, 0, 0, path, orientation, false, 0, 0, 0); + photoEntry.isVideo = isVideo; + photoEntry.thumbPath = thumb; + arrayList.add(photoEntry); + PhotoViewer.getInstance().setParentActivity(parentFragment.getParentActivity()); + PhotoViewer.getInstance().openPhotoForSelect(arrayList, 0, PhotoViewer.SELECT_TYPE_AVATAR, false, new PhotoViewer.EmptyPhotoViewerProvider() { + @Override + public void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo, boolean notify, int scheduleDate) { + String path = null; + MediaController.PhotoEntry photoEntry = (MediaController.PhotoEntry) arrayList.get(0); + if (photoEntry.imagePath != null) { + path = photoEntry.imagePath; + } else if (photoEntry.path != null) { + path = photoEntry.path; + } + MessageObject avatarObject = null; + Bitmap bitmap; + if (photoEntry.isVideo || photoEntry.editedInfo != null) { + TLRPC.TL_message message = new TLRPC.TL_message(); + message.id = 0; + message.message = ""; + message.media = new TLRPC.TL_messageMediaEmpty(); + message.action = new TLRPC.TL_messageActionEmpty(); + message.dialog_id = 0; + avatarObject = new MessageObject(UserConfig.selectedAccount, message, false); + avatarObject.messageOwner.attachPath = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_avatar.mp4").getAbsolutePath(); + avatarObject.videoEditedInfo = photoEntry.editedInfo; + bitmap = ImageLoader.loadBitmap(photoEntry.thumbPath, null, 800, 800, true); + } else { + bitmap = ImageLoader.loadBitmap(path, null, 800, 800, true); + } + processBitmap(bitmap, avatarObject); + } + + @Override + public boolean allowCaption() { + return false; + } + + @Override + public boolean canScrollAway() { + return false; + } + }, null); + } + public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { - if (requestCode == 13) { + if (requestCode == 0 || requestCode == 2) { + createChatAttachView(); + if (chatAttachAlert != null) { + chatAttachAlert.onActivityResultFragment(requestCode, data, currentPicturePath); + } + currentPicturePath = null; + } else if (requestCode == 13) { PhotoViewer.getInstance().setParentActivity(parentFragment.getParentActivity()); int orientation = 0; try { @@ -375,32 +686,7 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega } catch (Exception e) { FileLog.e(e); } - final ArrayList arrayList = new ArrayList<>(); - arrayList.add(new MediaController.PhotoEntry(0, 0, 0, currentPicturePath, orientation, false, 0, 0, 0)); - PhotoViewer.getInstance().openPhotoForSelect(arrayList, 0, PhotoViewer.SELECT_TYPE_AVATAR, false, new PhotoViewer.EmptyPhotoViewerProvider() { - @Override - public void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo, boolean notify, int scheduleDate) { - String path = null; - MediaController.PhotoEntry photoEntry = (MediaController.PhotoEntry) arrayList.get(0); - if (photoEntry.imagePath != null) { - path = photoEntry.imagePath; - } else if (photoEntry.path != null) { - path = photoEntry.path; - } - Bitmap bitmap = ImageLoader.loadBitmap(path, null, 800, 800, true); - processBitmap(bitmap); - } - - @Override - public boolean allowCaption() { - return false; - } - - @Override - public boolean canScrollAway() { - return false; - } - }, null); + openPhotoForEdit(currentPicturePath, null, orientation, false); AndroidUtilities.addMediaToGallery(currentPicturePath); currentPicturePath = null; } else if (requestCode == 14) { @@ -408,14 +694,22 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega return; } startCrop(null, data.getData()); + } else if (requestCode == 15) { + openPhotoForEdit(currentPicturePath, null, 0, true); + AndroidUtilities.addMediaToGallery(currentPicturePath); + currentPicturePath = null; } } } - private void processBitmap(Bitmap bitmap) { + private void processBitmap(Bitmap bitmap, MessageObject avatarObject) { if (bitmap == null) { return; } + uploadedVideo = null; + uploadedPhoto = null; + convertingVideo = null; + videoPath = null; bigPhoto = ImageLoader.scaleAndSaveImage(bitmap, 800, 800, 80, false, 320, 320); smallPhoto = ImageLoader.scaleAndSaveImage(bitmap, 150, 150, 80, false, 150, 150); if (smallPhoto != null) { @@ -432,49 +726,90 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega UserConfig.getInstance(currentAccount).saveConfig(false); uploadingImage = FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE) + "/" + bigPhoto.location.volume_id + "_" + bigPhoto.location.local_id + ".jpg"; if (uploadAfterSelect) { + if (avatarObject != null && avatarObject.videoEditedInfo != null) { + convertingVideo = avatarObject; + long startTime = avatarObject.videoEditedInfo.startTime < 0 ? 0 : avatarObject.videoEditedInfo.startTime; + videoTimestamp = (avatarObject.videoEditedInfo.avatarStartTime - startTime) / 1000000.0; + NotificationCenter.getInstance(currentAccount).addObserver(ImageUpdater.this, NotificationCenter.filePreparingStarted); + NotificationCenter.getInstance(currentAccount).addObserver(ImageUpdater.this, NotificationCenter.filePreparingFailed); + NotificationCenter.getInstance(currentAccount).addObserver(ImageUpdater.this, NotificationCenter.fileNewChunkAvailable); + MediaController.getInstance().scheduleVideoConvert(avatarObject, true); + uploadingImage = null; + if (delegate != null) { + delegate.didStartUpload(true); + } + } else { + if (delegate != null) { + delegate.didStartUpload(false); + } + } NotificationCenter.getInstance(currentAccount).addObserver(ImageUpdater.this, NotificationCenter.FileDidUpload); + NotificationCenter.getInstance(currentAccount).addObserver(ImageUpdater.this, NotificationCenter.FileUploadProgressChanged); NotificationCenter.getInstance(currentAccount).addObserver(ImageUpdater.this, NotificationCenter.FileDidFailUpload); - FileLoader.getInstance(currentAccount).uploadFile(uploadingImage, false, true, ConnectionsManager.FileTypePhoto); + if (uploadingImage != null) { + FileLoader.getInstance(currentAccount).uploadFile(uploadingImage, false, true, ConnectionsManager.FileTypePhoto); + } } if (delegate != null) { - delegate.didUploadPhoto(null, bigPhoto, smallPhoto); + delegate.didUploadPhoto(null, null, 0, null, bigPhoto, smallPhoto); } } } @Override public void didFinishEdit(Bitmap bitmap) { - processBitmap(bitmap); + processBitmap(bitmap, null); + } + + private void cleanup() { + uploadingImage = null; + uploadingVideo = null; + videoPath = null; + convertingVideo = null; + if (clearAfterUpdate) { + imageReceiver.setImageBitmap((Drawable) null); + parentFragment = null; + delegate = null; + } } @Override public void didReceivedNotification(int id, int account, Object... args) { - if (id == NotificationCenter.FileDidUpload) { + if (id == NotificationCenter.FileDidUpload || id == NotificationCenter.FileDidFailUpload) { String location = (String) args[0]; if (location.equals(uploadingImage)) { - NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.FileDidUpload); - NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.FileDidFailUpload); - if (delegate != null) { - delegate.didUploadPhoto((TLRPC.InputFile) args[1], bigPhoto, smallPhoto); - } uploadingImage = null; - if (clearAfterUpdate) { - imageReceiver.setImageBitmap((Drawable) null); - parentFragment = null; - delegate = null; + if (id == NotificationCenter.FileDidUpload) { + uploadedPhoto = (TLRPC.InputFile) args[1]; } + } else if (location.equals(uploadingVideo)) { + uploadingVideo = null; + if (id == NotificationCenter.FileDidUpload) { + uploadedVideo = (TLRPC.InputFile) args[1]; + } + } else { + return; } - } else if (id == NotificationCenter.FileDidFailUpload) { - String location = (String) args[0]; - if (location.equals(uploadingImage)) { + + if (uploadingImage == null && uploadingVideo == null && convertingVideo == null) { NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.FileDidUpload); + NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.FileUploadProgressChanged); NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.FileDidFailUpload); - uploadingImage = null; - if (clearAfterUpdate) { - imageReceiver.setImageBitmap((Drawable) null); - parentFragment = null; - delegate = null; + if (id == NotificationCenter.FileDidUpload) { + if (delegate != null) { + delegate.didUploadPhoto(uploadedPhoto, uploadedVideo, videoTimestamp, videoPath, bigPhoto, smallPhoto); + } } + cleanup(); + } + } else if (id == NotificationCenter.FileUploadProgressChanged) { + String location = (String) args[0]; + String path = convertingVideo != null ? uploadingVideo : uploadingImage; + if (delegate != null && location.equals(path)) { + Long loadedSize = (Long) args[1]; + Long totalSize = (Long) args[2]; + float progress = Math.min(1f, loadedSize / (float) totalSize); + delegate.onUploadProgressChanged(progress); } } else if (id == NotificationCenter.fileDidLoad || id == NotificationCenter.fileDidFailToLoad || id == NotificationCenter.httpFileDidLoad || id == NotificationCenter.httpFileDidFailedLoad) { String path = (String) args[0]; @@ -487,11 +822,73 @@ public class ImageUpdater implements NotificationCenter.NotificationCenterDelega uploadingImage = null; if (id == NotificationCenter.fileDidLoad || id == NotificationCenter.httpFileDidLoad) { Bitmap bitmap = ImageLoader.loadBitmap(finalPath, null, 800, 800, true); - processBitmap(bitmap); + processBitmap(bitmap, null); } else { imageReceiver.setImageBitmap((Drawable) null); } } + } else if (id == NotificationCenter.filePreparingFailed) { + MessageObject messageObject = (MessageObject) args[0]; + if (messageObject != convertingVideo || parentFragment == null) { + return; + } + parentFragment.getSendMessagesHelper().stopVideoService(messageObject.messageOwner.attachPath); + NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.filePreparingStarted); + NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.filePreparingFailed); + NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.fileNewChunkAvailable); + cleanup(); + } else if (id == NotificationCenter.fileNewChunkAvailable) { + MessageObject messageObject = (MessageObject) args[0]; + if (messageObject != convertingVideo || parentFragment == null) { + return; + } + String finalPath = (String) args[1]; + long availableSize = (Long) args[2]; + long finalSize = (Long) args[3]; + parentFragment.getFileLoader().checkUploadNewDataAvailable(finalPath, false, availableSize, finalSize); + if (finalSize != 0) { + double lastFrameTimestamp = ((Long) args[5]) / 1000000.0; + if (videoTimestamp > lastFrameTimestamp) { + videoTimestamp = lastFrameTimestamp; + } + + Bitmap bitmap = SendMessagesHelper.createVideoThumbnailAtTime(finalPath, (long) (videoTimestamp * 1000), null, true); + if (bitmap != null) { + File path = FileLoader.getPathToAttach(smallPhoto, true); + if (path != null) { + path.delete(); + } + path = FileLoader.getPathToAttach(bigPhoto, true); + if (path != null) { + path.delete(); + } + bigPhoto = ImageLoader.scaleAndSaveImage(bitmap, 800, 800, 80, false, 320, 320); + smallPhoto = ImageLoader.scaleAndSaveImage(bitmap, 150, 150, 80, false, 150, 150); + if (smallPhoto != null) { + try { + Bitmap b = BitmapFactory.decodeFile(FileLoader.getPathToAttach(smallPhoto, true).getAbsolutePath()); + String key = smallPhoto.location.volume_id + "_" + smallPhoto.location.local_id + "@50_50"; + ImageLoader.getInstance().putImageToCache(new BitmapDrawable(b), key); + } catch (Throwable ignore) { + + } + } + } + + NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.filePreparingStarted); + NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.filePreparingFailed); + NotificationCenter.getInstance(currentAccount).removeObserver(ImageUpdater.this, NotificationCenter.fileNewChunkAvailable); + parentFragment.getSendMessagesHelper().stopVideoService(messageObject.messageOwner.attachPath); + uploadingVideo = videoPath = finalPath; + convertingVideo = null; + } + } else if (id == NotificationCenter.filePreparingStarted) { + MessageObject messageObject = (MessageObject) args[0]; + if (messageObject != convertingVideo || parentFragment == null) { + return; + } + uploadingVideo = (String) args[1]; + parentFragment.getFileLoader().uploadFile(uploadingVideo, false, false, (int) convertingVideo.videoEditedInfo.estimatedSize, ConnectionsManager.FileTypeVideo); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/InstantCameraView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/InstantCameraView.java index c845d0b9d..b16ffbffe 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/InstantCameraView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/InstantCameraView.java @@ -90,6 +90,7 @@ import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; +import java.nio.IntBuffer; import java.util.ArrayList; import java.util.Timer; import java.util.TimerTask; @@ -1511,13 +1512,13 @@ public class InstantCameraView extends FrameLayout implements NotificationCenter private int scaleYHandle; private int alphaHandle; private int zeroTimeStamps; - private Integer lastCameraId = 0; private AudioRecord audioRecorder; private ArrayBlockingQueue buffers = new ArrayBlockingQueue<>(10); private ArrayList keyframeThumbs = new ArrayList<>(); + private DispatchQueue generateKeyframeThumbsQueue; private int frameCount; private Runnable recorderRunnable = new Runnable() { @@ -1643,6 +1644,11 @@ public class InstantCameraView extends FrameLayout implements NotificationCenter } keyframeThumbs.clear(); frameCount = 0; + if (generateKeyframeThumbsQueue != null) { + generateKeyframeThumbsQueue.cleanupQueue(); + generateKeyframeThumbsQueue.recycle(); + } + generateKeyframeThumbsQueue = new DispatchQueue("keyframes_thumb_queque"); handler.sendMessage(handler.obtainMessage(MSG_START_RECORDING)); } @@ -1883,20 +1889,9 @@ public class InstantCameraView extends FrameLayout implements NotificationCenter EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, currentTimestamp); EGL14.eglSwapBuffers(eglDisplay, eglSurface); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && frameCount % 30 == 0) { - final TextureView textureViewFinal = textureView; - if (textureViewFinal != null) { - Bitmap bitmap = textureViewFinal.getBitmap( AndroidUtilities.dp(56), AndroidUtilities.dp(56)); - if ((bitmap == null || bitmap.getPixel(0,0) == 0) && keyframeThumbs.size() > 1) { - keyframeThumbs.add(keyframeThumbs.get(keyframeThumbs.size() - 1)); - } else { - keyframeThumbs.add(bitmap); - } - } - } + createKeyframeThumb(); frameCount++; - if (oldCameraTexture[0] != 0 && cameraTextureAlpha < 1.0f) { cameraTextureAlpha += alphaDt / 200000000.0f; if (cameraTextureAlpha > 1) { @@ -1916,6 +1911,30 @@ public class InstantCameraView extends FrameLayout implements NotificationCenter } } + private void createKeyframeThumb() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && frameCount % 33 == 0) { + GenerateKeyframeThumbTask task = new GenerateKeyframeThumbTask(); + generateKeyframeThumbsQueue.postRunnable(task); + } + } + + private class GenerateKeyframeThumbTask implements Runnable { + @Override + public void run() { + final TextureView textureView = InstantCameraView.this.textureView; + if (textureView != null) { + final Bitmap bitmap = textureView.getBitmap(AndroidUtilities.dp(56), AndroidUtilities.dp(56)); + AndroidUtilities.runOnUIThread(() -> { + if ((bitmap == null || bitmap.getPixel(0, 0) == 0) && keyframeThumbs.size() > 1) { + keyframeThumbs.add(keyframeThumbs.get(keyframeThumbs.size() - 1)); + } else { + keyframeThumbs.add(bitmap); + } + }); + } + } + } + private void handleStopRecording(final int send) { if (running) { sendWhenDone = send; @@ -1952,6 +1971,11 @@ public class InstantCameraView extends FrameLayout implements NotificationCenter FileLog.e(e); } } + if (generateKeyframeThumbsQueue != null) { + generateKeyframeThumbsQueue.cleanupQueue(); + generateKeyframeThumbsQueue.recycle(); + generateKeyframeThumbsQueue = null; + } if (send != 0) { AndroidUtilities.runOnUIThread(() -> { videoEditedInfo = new VideoEditedInfo(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/IntSeekBarAccessibilityDelegate.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/IntSeekBarAccessibilityDelegate.java new file mode 100644 index 000000000..fb5d14bf8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/IntSeekBarAccessibilityDelegate.java @@ -0,0 +1,39 @@ +package org.telegram.ui.Components; + +import android.view.View; + +public abstract class IntSeekBarAccessibilityDelegate extends SeekBarAccessibilityDelegate { + + @Override + protected void doScroll(View host, boolean backward) { + int delta = getDelta(); + if (backward) { + delta *= -1; + } + setProgress(Math.min(getMaxValue(), Math.max(getMinValue(), getProgress() + delta))); + } + + @Override + protected boolean canScrollBackward(View host) { + return getProgress() > getMinValue(); + } + + @Override + protected boolean canScrollForward(View host) { + return getProgress() < getMaxValue(); + } + + protected abstract int getProgress(); + + protected abstract void setProgress(int progress); + + protected int getMinValue() { + return 0; + } + + protected abstract int getMaxValue(); + + protected int getDelta() { + return 1; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/MediaActionDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/MediaActionDrawable.java index c7d681eb6..89e5e2e45 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/MediaActionDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/MediaActionDrawable.java @@ -2,7 +2,6 @@ package org.telegram.ui.Components; import android.graphics.Canvas; import android.graphics.ColorFilter; -import android.graphics.CornerPathEffect; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; @@ -41,22 +40,11 @@ public class MediaActionDrawable extends Drawable { private Paint backPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint paint2 = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint paint3 = new Paint(Paint.ANTI_ALIAS_FLAG); - private Path path1 = new Path(); - private Path path2 = new Path(); private RectF rect = new RectF(); private ColorFilter colorFilter; private float scale = 1.0f; private DecelerateInterpolator interpolator = new DecelerateInterpolator(); - private final static float[] playPath1 = new float[] {18, 15, 34, 24, 34, 24, 18, 24, 18, 24}; - private final static float[] playPath2 = new float[] {18, 33, 34, 24, 34, 24, 18, 24, 18, 24}; - private final static float[] playFinalPath = new float[] {18, 15, 34, 24, 18, 33}; - private final static int playRotation = 0; - - private final static float[] pausePath1 = new float[] {16, 17, 32, 17, 32, 22, 16, 22, 16, 19.5f}; - private final static float[] pausePath2 = new float[] {16, 31, 32, 31, 32, 26, 16, 26, 16, 28.5f}; - private final static int pauseRotation = 90; - private boolean isMini; private float transitionAnimationTime = 400.0f; @@ -111,7 +99,6 @@ public class MediaActionDrawable extends Drawable { textPaint.setColor(0xffffffff); paint2.setColor(0xffffffff); - paint2.setPathEffect(new CornerPathEffect(AndroidUtilities.dp(2))); } @Override @@ -170,7 +157,9 @@ public class MediaActionDrawable extends Drawable { if (currentIcon == icon || nextIcon == icon) { return false; } - if (currentIcon == ICON_DOWNLOAD && (icon == ICON_CANCEL || icon == ICON_CANCEL_FILL)) { + if (currentIcon == ICON_PLAY && icon == ICON_PAUSE || currentIcon == ICON_PAUSE && icon == ICON_PLAY) { + transitionAnimationTime = 666.0f; + } else if (currentIcon == ICON_DOWNLOAD && (icon == ICON_CANCEL || icon == ICON_CANCEL_FILL)) { transitionAnimationTime = 400.0f; } else if (currentIcon != ICON_NONE && icon == ICON_CHECK) { transitionAnimationTime = 360.0f; @@ -228,6 +217,10 @@ public class MediaActionDrawable extends Drawable { invalidateSelf(); } + public float getProgress() { + return downloadProgress; + } + private float getCircleValue(float value) { while (value > 360) { value -= 360; @@ -447,11 +440,10 @@ public class MediaActionDrawable extends Drawable { float backProgress; if (nextIcon == ICON_CHECK) { progress = Math.min(1.0f, transitionProgress / CANCEL_TO_CHECK_STAGE1); - backProgress = 1.0f - progress; } else { progress = transitionProgress; - backProgress = 1.0f - progress; } + backProgress = 1.0f - progress; rotation = 45 * progress; d = AndroidUtilities.dp(7) * backProgress * scale; alpha = (int) (255 * Math.min(1.0f, backProgress * 2.0f)); @@ -656,115 +648,61 @@ public class MediaActionDrawable extends Drawable { if (currentIcon == ICON_PLAY || currentIcon == ICON_PAUSE || nextIcon == ICON_PLAY || nextIcon == ICON_PAUSE) { float p; if (currentIcon == ICON_PLAY && nextIcon == ICON_PAUSE || currentIcon == ICON_PAUSE && nextIcon == ICON_PLAY) { - p = animatingTransition ? interpolator.getInterpolation(transitionProgress) : 0.0f; + if (animatingTransition) { + if (nextIcon == ICON_PLAY) { + p = 1.0f - transitionProgress; + } else { + p = transitionProgress; + } + } else { + p = nextIcon == ICON_PAUSE ? 1.0f : 0.0f; + } } else { - p = 0.0f; + p = currentIcon == ICON_PAUSE ? 1.0f : 0.0f; } - path1.reset(); - path2.reset(); - - float[] p1; - float[] p2; - - float[] p3; - float[] p4; - - float[] finalPath = null; - - int rotation1; - int rotation2; - - switch (currentIcon) { - case ICON_PLAY: - p1 = playPath1; - p2 = playPath2; - finalPath = playFinalPath; - rotation1 = playRotation; - break; - case ICON_PAUSE: - p1 = pausePath1; - p2 = pausePath2; - rotation1 = pauseRotation; - break; - default: - rotation1 = 0; - p1 = p2 = null; - break; - } - - switch (nextIcon) { - case ICON_PLAY: - p3 = playPath1; - p4 = playPath2; - rotation2 = playRotation; - break; - case ICON_PAUSE: - p3 = pausePath1; - p4 = pausePath2; - rotation2 = pauseRotation; - break; - default: - rotation2 = 0; - p3 = p4 = null; - break; - } - - if (p1 == null) { - p1 = p3; - p2 = p4; - p3 = null; - p4 = null; - } - if (!animatingTransition && finalPath != null) { - for (int a = 0; a < finalPath.length / 2; a++) { - if (a == 0) { - path1.moveTo(AndroidUtilities.dp(finalPath[a * 2]) * scale, AndroidUtilities.dp(finalPath[a * 2 + 1]) * scale); - path2.moveTo(AndroidUtilities.dp(finalPath[a * 2]) * scale, AndroidUtilities.dp(finalPath[a * 2 + 1]) * scale); - } else { - path1.lineTo(AndroidUtilities.dp(finalPath[a * 2]) * scale, AndroidUtilities.dp(finalPath[a * 2 + 1]) * scale); - path2.lineTo(AndroidUtilities.dp(finalPath[a * 2]) * scale, AndroidUtilities.dp(finalPath[a * 2 + 1]) * scale); - } - } - } else if (p3 == null) { - for (int a = 0; a < 5; a++) { - if (a == 0) { - path1.moveTo(AndroidUtilities.dp(p1[a * 2]) * scale, AndroidUtilities.dp(p1[a * 2 + 1]) * scale); - path2.moveTo(AndroidUtilities.dp(p2[a * 2]) * scale, AndroidUtilities.dp(p2[a * 2 + 1]) * scale); - } else { - path1.lineTo(AndroidUtilities.dp(p1[a * 2]) * scale, AndroidUtilities.dp(p1[a * 2 + 1]) * scale); - path2.lineTo(AndroidUtilities.dp(p2[a * 2]) * scale, AndroidUtilities.dp(p2[a * 2 + 1]) * scale); - } - } + if (nextIcon != ICON_PLAY && nextIcon != ICON_PAUSE || currentIcon != ICON_PLAY && currentIcon != ICON_PAUSE) { if (nextIcon == ICON_NONE) { paint2.setAlpha((int) (255 * (1.0f - transitionProgress))); } else { paint2.setAlpha(currentIcon == nextIcon ? 255 : (int) (transitionProgress * 255)); } } else { - for (int a = 0; a < 5; a++) { - if (a == 0) { - path1.moveTo(AndroidUtilities.dp(p1[a * 2] + (p3[a * 2] - p1[a * 2]) * p) * scale, AndroidUtilities.dp(p1[a * 2 + 1] + (p3[a * 2 + 1] - p1[a * 2 + 1]) * p) * scale); - path2.moveTo(AndroidUtilities.dp(p2[a * 2] + (p4[a * 2] - p2[a * 2]) * p) * scale, AndroidUtilities.dp(p2[a * 2 + 1] + (p4[a * 2 + 1] - p2[a * 2 + 1]) * p) * scale); - } else { - path1.lineTo(AndroidUtilities.dp(p1[a * 2] + (p3[a * 2] - p1[a * 2]) * p) * scale, AndroidUtilities.dp(p1[a * 2 + 1] + (p3[a * 2 + 1] - p1[a * 2 + 1]) * p) * scale); - path2.lineTo(AndroidUtilities.dp(p2[a * 2] + (p4[a * 2] - p2[a * 2]) * p) * scale, AndroidUtilities.dp(p2[a * 2 + 1] + (p4[a * 2 + 1] - p2[a * 2 + 1]) * p) * scale); - } - } paint2.setAlpha(255); } - path1.close(); - path2.close(); - canvas.save(); - canvas.translate(bounds.left, bounds.top); - canvas.rotate(rotation1 + (rotation2 - rotation1) * p, cx - bounds.left, cy - bounds.top); - if (currentIcon != ICON_PLAY && currentIcon != ICON_PAUSE || currentIcon == ICON_NONE) { - canvas.scale(drawableScale, drawableScale, cx - bounds.left, cy - bounds.top); + canvas.translate(bounds.centerX() + AndroidUtilities.dp(1) * (1.0f - p), bounds.centerY()); + float ms = 666.0f * p; + float rotation = currentIcon == ICON_PAUSE ? 90 : 0; + float iconScale = 1.0f; + if (currentIcon == ICON_PLAY && nextIcon == ICON_PAUSE || currentIcon == ICON_PAUSE && nextIcon == ICON_PLAY) { + if (ms < 100) { + rotation = -5 * CubicBezierInterpolator.EASE_BOTH.getInterpolation(ms / 100.0f); + } else if (ms < 484) { + rotation = -5 + 95 * CubicBezierInterpolator.EASE_BOTH.getInterpolation((ms - 100) / 384); + } else { + rotation = 90; + } + if (ms < 200) { + iconScale = 1.0f; + } else if (ms < 416) { + iconScale = 1.0f + 0.1f * CubicBezierInterpolator.EASE_BOTH.getInterpolation((ms - 200) / 216.0f); + } else if (ms < 566) { + iconScale = 1.1f - 0.13f * CubicBezierInterpolator.EASE_BOTH.getInterpolation((ms - 416) / 150.0f); + } else { + iconScale = 0.97f + 0.03f * interpolator.getInterpolation((ms - 566) / 100.0f); + } } - canvas.drawPath(path1, paint2); - canvas.drawPath(path2, paint2); + canvas.rotate(rotation); + canvas.scale(iconScale, iconScale); + if (currentIcon != ICON_PLAY && currentIcon != ICON_PAUSE || currentIcon == ICON_NONE) { + canvas.scale(drawableScale, drawableScale); + } + Theme.playPauseAnimator.draw(canvas, paint2, ms); + canvas.scale(1.0f, -1.0f); + Theme.playPauseAnimator.draw(canvas, paint2, ms); + canvas.restore(); } if (currentIcon == ICON_CHECK || nextIcon == ICON_CHECK) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberPicker.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberPicker.java index dbe38c6b8..dff6717a1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberPicker.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberPicker.java @@ -104,6 +104,7 @@ public class NumberPicker extends LinearLayout { private boolean mDecrementVirtualButtonPressed; private PressedStateHelper mPressedStateHelper; private int mLastHandledDownDpadKeyCode = -1; + private SeekBarAccessibilityDelegate accessibilityDelegate; public interface OnValueChangeListener { void onValueChange(NumberPicker picker, int oldVal, int newVal); @@ -188,6 +189,33 @@ public class NumberPicker extends LinearLayout { mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); updateInputTextView(); + + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + setAccessibilityDelegate(accessibilityDelegate = new SeekBarAccessibilityDelegate() { + @Override + protected void doScroll(View host, boolean backward) { + changeValueByOne(!backward); + } + + @Override + protected boolean canScrollBackward(View host) { + return true; + } + + @Override + protected boolean canScrollForward(View host) { + return true; + } + + @Override + public CharSequence getContentDescription(View host) { + return NumberPicker.this.getContentDescription(mValue); + } + }); + } + + protected CharSequence getContentDescription(int value) { + return mInputText.getText(); } public void setTextColor(int color) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Brush.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Brush.java index 3d434f697..dc633bb2f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Brush.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Brush.java @@ -119,4 +119,39 @@ public interface Brush { return BitmapFactory.decodeResource(ApplicationLoader.applicationContext.getResources(), R.drawable.paint_neon_brush, options); } } + + class Arrow implements Brush { + + @Override + public float getSpacing() { + return 0.15f; + } + + @Override + public float getAlpha() { + return 0.85f; + } + + @Override + public float getAngle() { + return 0.0f; + } + + @Override + public float getScale() { + return 1.0f; + } + + @Override + public boolean isLightSaber() { + return false; + } + + @Override + public Bitmap getStamp() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + return BitmapFactory.decodeResource(ApplicationLoader.applicationContext.getResources(), R.drawable.paint_radial_brush, options); + } + } } \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/GLMatrix.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/GLMatrix.java index f6ef6b83e..f3f4d0ab2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/GLMatrix.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/GLMatrix.java @@ -12,7 +12,7 @@ public class GLMatrix { float ty = -(top + bottom) / (top - bottom); float tz = -(far + near) / (far - near); - float out[] = new float[16]; + float[] out = new float[16]; out[0] = 2.0f / r_l; out[1] = 0.0f; @@ -38,8 +38,8 @@ public class GLMatrix { } public static float[] LoadGraphicsMatrix(Matrix matrix) { - float m[] = new float[16]; - float v[] = new float[9]; + float[] m = new float[16]; + float[] v = new float[9]; matrix.getValues(v); m[0] = v[Matrix.MSCALE_X]; //m.a; @@ -66,7 +66,7 @@ public class GLMatrix { } public static float[] MultiplyMat4f(float[] a, float[] b) { - float out[] = new float[16]; + float[] out = new float[16]; out[0] = a[0] * b[0] + a[4] * b[1] + a[8] * b[2] + a[12] * b[3]; out[1] = a[1] * b[0] + a[5] * b[1] + a[9] * b[2] + a[13] * b[3]; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Input.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Input.java index 79358738f..46f148e98 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Input.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Input.java @@ -18,6 +18,7 @@ public class Input { private Point lastLocation; private double lastRemainder; + private float lastAngle; private Point[] points = new Point[3]; private int pointsCount; @@ -74,6 +75,7 @@ public class Input { pointsCount++; if (pointsCount == 3) { + lastAngle = (float) Math.atan2(points[2].y - points[1].y, points[2].x - points[1].x); smoothenAndPaintPoints(false); } @@ -90,6 +92,24 @@ public class Input { reset(); } else if (pointsCount > 0) { smoothenAndPaintPoints(true); + + Brush brush = renderView.getCurrentBrush(); + if (brush instanceof Brush.Arrow) { + float arrowLength = renderView.getCurrentWeight() * 4.5f; + float angle = lastAngle; + location = points[pointsCount - 1]; + + Point tip = new Point(location.x, location.y, 0.8f); + Point leftTip = new Point(location.x + Math.cos(angle - Math.PI / 4 * 3) * arrowLength, location.y + Math.sin(angle - Math.PI / 4 * 3.2) * arrowLength, 1.0); + leftTip.edge = true; + Path left = new Path(new Point[]{tip, leftTip}); + paintPath(left); + + Point rightTip = new Point(location.x + Math.cos(angle + Math.PI / 4 * 3) * arrowLength, location.y + Math.sin(angle + Math.PI / 4 * 3.2) * arrowLength, 1.0); + rightTip.edge = true; + Path right = new Path(new Point[]{tip, rightTip}); + paintPath(right); + } } pointsCount = 0; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/EntityView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/EntityView.java index b0d1dcfd1..885c608df 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/EntityView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/EntityView.java @@ -23,6 +23,9 @@ public class EntityView extends FrameLayout { boolean onEntitySelected(EntityView entityView); boolean onEntityLongClicked(EntityView entityView); boolean allowInteraction(EntityView entityView); + int[] getCenterLocation(EntityView entityView); + float[] getTransformedTouch(float x, float y); + float getCropRotation(); } private float previousLocationX; @@ -129,8 +132,7 @@ public class EntityView extends FrameLayout { return false; } - float x = event.getRawX(); - float y = event.getRawY(); + float[] xy = delegate.getTransformedTouch(event.getRawX(), event.getRawY()); int action = event.getActionMasked(); boolean handled = false; @@ -141,15 +143,15 @@ public class EntityView extends FrameLayout { delegate.onEntitySelected(this); announcedSelection = true; } - previousLocationX = x; - previousLocationY = y; + previousLocationX = xy[0]; + previousLocationY = xy[1]; handled = true; hasReleased = false; } break; case MotionEvent.ACTION_MOVE: { - handled = onTouchMove(x, y); + handled = onTouchMove(xy[0], xy[1]); } break; @@ -278,14 +280,19 @@ public class EntityView extends FrameLayout { int action = event.getActionMasked(); boolean handled = false; + float rawX = event.getRawX(); + float rawY = event.getRawY(); + float[] xy = delegate.getTransformedTouch(rawX, rawY); + float x = xy[0]; + float y = xy[1]; switch (action) { case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_DOWN: { int handle = pointInsideHandle(event.getX(), event.getY()); if (handle != 0) { currentHandle = handle; - previousLocationX = event.getRawX(); - previousLocationY = event.getRawY(); + previousLocationX = x; + previousLocationY = y; hasReleased = false; handled = true; } @@ -294,15 +301,11 @@ public class EntityView extends FrameLayout { case MotionEvent.ACTION_MOVE: { if (currentHandle == SELECTION_WHOLE_HANDLE) { - float x = event.getRawX(); - float y = event.getRawY(); - handled = onTouchMove(x, y); - } - else if (currentHandle != 0) { + } else if (currentHandle != 0) { - float tx = event.getRawX() - previousLocationX; - float ty = event.getRawY() - previousLocationY; + float tx = x - previousLocationX; + float ty = y - previousLocationY; if (hasTransformed || Math.abs(tx) > AndroidUtilities.dp(2) || Math.abs(ty) > AndroidUtilities.dp(2)) { hasTransformed = true; @@ -315,23 +318,18 @@ public class EntityView extends FrameLayout { float scaleDelta = 1 + (delta * 2) / getMeasuredWidth(); scale(scaleDelta); - float centerX = getLeft() + getMeasuredWidth() / 2; - float centerY = getTop() + getMeasuredHeight() / 2; - - float parentX = event.getRawX() - ((View) getParent()).getLeft(); - float parentY = event.getRawY() - ((View) getParent()).getTop(); - + int[] pos = delegate.getCenterLocation(EntityView.this); float angle = 0; if (currentHandle == SELECTION_LEFT_HANDLE) { - angle = (float) Math.atan2(centerY - parentY, centerX - parentX); + angle = (float) Math.atan2(pos[1] - rawY, pos[0] - rawX); } else if (currentHandle == SELECTION_RIGHT_HANDLE) { - angle = (float) Math.atan2(parentY - centerY, parentX - centerX); + angle = (float) Math.atan2(rawY - pos[1], rawX - pos[0]); } - rotate((float) Math.toDegrees(angle)); + rotate((float) Math.toDegrees(angle) - delegate.getCropRotation()); - previousLocationX = event.getRawX(); - previousLocationY = event.getRawY(); + previousLocationX = x; + previousLocationY = y; } handled = true; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/StickerView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/StickerView.java index 38fd66413..26f5df890 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/StickerView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Paint/Views/StickerView.java @@ -72,6 +72,14 @@ public class StickerView extends EntityView { centerImage.setParentView(containerView); TLRPC.PhotoSize thumb = FileLoader.getClosestPhotoSizeWithSize(sticker.thumbs, 90); centerImage.setImage(ImageLocation.getForDocument(sticker), null, ImageLocation.getForDocument(thumb, sticker), null, "webp", parentObject, 1); + centerImage.setDelegate((imageReceiver, set, isThumb, memCache) -> { + if (set && !isThumb) { + RLottieDrawable drawable = imageReceiver.getLottieAnimation(); + if (drawable != null) { + didSetAnimatedSticker(drawable); + } + } + }); updatePosition(); } @@ -116,6 +124,10 @@ public class StickerView extends EntityView { updateSelectionView(); } + protected void didSetAnimatedSticker(RLottieDrawable drawable) { + + } + protected void stickerDraw(Canvas canvas) { if (containerView == null) { return; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PaintingOverlay.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PaintingOverlay.java index ef48e17e4..8a7587146 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PaintingOverlay.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PaintingOverlay.java @@ -227,6 +227,14 @@ public class PaintingOverlay extends FrameLayout { return paintBitmap; } + @Override + public void setAlpha(float alpha) { + super.setAlpha(alpha); + if (backgroundDrawable != null) { + backgroundDrawable.setAlpha((int) (255 * alpha)); + } + } + public Bitmap getThumb() { float w = getMeasuredWidth(); float h = getMeasuredHeight(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PathAnimator.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PathAnimator.java new file mode 100644 index 000000000..10a4c2634 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PathAnimator.java @@ -0,0 +1,170 @@ +/* + * This is the source code of Telegram for Android v. 6.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-2020. + */ + +package org.telegram.ui.Components; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.FileLog; + +import java.util.ArrayList; + +public class PathAnimator { + + private Path path = new Path(); + private float scale; + private float tx; + private float ty; + private ArrayList keyFrames = new ArrayList<>(); + + private static class KeyFrame { + public ArrayList commands = new ArrayList<>(); + public float time; + } + + private static class MoveTo { + public float x; + public float y; + } + + private static class LineTo { + public float x; + public float y; + } + + private static class CurveTo { + public float x; + public float y; + public float x1; + public float y1; + public float x2; + public float y2; + } + + public PathAnimator(float sc, float x, float y) { + scale = sc; + tx = x; + ty = y; + } + + public void addSvgKeyFrame(String svg, float ms) { + if (svg == null) { + return; + } + try { + KeyFrame keyFrame = new KeyFrame(); + keyFrame.time = ms; + String[] args = svg.split(" "); + for (int a = 0; a < args.length; a++) { + switch (args[a].charAt(0)) { + case 'M': { + MoveTo moveTo = new MoveTo(); + moveTo.x = (Float.parseFloat(args[a + 1]) + tx) * scale; + moveTo.y = (Float.parseFloat(args[a + 2]) + ty) * scale; + keyFrame.commands.add(moveTo); + a += 2; + break; + } + case 'C': { + CurveTo curveTo = new CurveTo(); + curveTo.x1 = (Float.parseFloat(args[a + 1]) + tx) * scale; + curveTo.y1 = (Float.parseFloat(args[a + 2]) + ty) * scale; + curveTo.x2 = (Float.parseFloat(args[a + 3]) + tx) * scale; + curveTo.y2 = (Float.parseFloat(args[a + 4]) + ty) * scale; + curveTo.x = (Float.parseFloat(args[a + 5]) + tx) * scale; + curveTo.y = (Float.parseFloat(args[a + 6]) + ty) * scale; + keyFrame.commands.add(curveTo); + a += 6; + break; + } + case 'L': { + LineTo lineTo = new LineTo(); + lineTo.x = (Float.parseFloat(args[a + 1]) + tx) * scale; + lineTo.y = (Float.parseFloat(args[a + 2]) + ty) * scale; + keyFrame.commands.add(lineTo); + a += 2; + break; + } + } + } + keyFrames.add(keyFrame); + } catch (Exception e) { + FileLog.e(e); + } + } + + public void draw(Canvas canvas, Paint paint, float time) { + KeyFrame startKeyFrame = null; + KeyFrame endKeyFrame = null; + for (int a = 0, N = keyFrames.size(); a < N; a++) { + KeyFrame keyFrame = keyFrames.get(a); + if ((startKeyFrame == null || startKeyFrame.time < keyFrame.time) && keyFrame.time <= time) { + startKeyFrame = keyFrame; + } + if ((endKeyFrame == null || endKeyFrame.time > keyFrame.time) && keyFrame.time >= time) { + endKeyFrame = keyFrame; + } + } + if (endKeyFrame == startKeyFrame) { + startKeyFrame = null; + } + if (startKeyFrame != null && endKeyFrame == null) { + endKeyFrame = startKeyFrame; + startKeyFrame = null; + } + if (endKeyFrame == null || startKeyFrame != null && startKeyFrame.commands.size() != endKeyFrame.commands.size()) { + return; + } + path.reset(); + for (int a = 0, N = endKeyFrame.commands.size(); a < N; a++) { + Object startCommand = startKeyFrame != null ? startKeyFrame.commands.get(a) : null; + Object endCommand = endKeyFrame.commands.get(a); + if (startCommand != null && startCommand.getClass() != endCommand.getClass()) { + return; + } + float progress; + if (startKeyFrame != null) { + progress = (time - startKeyFrame.time) / (endKeyFrame.time - startKeyFrame.time); + } else { + progress = 1.0f; + } + if (endCommand instanceof MoveTo) { + MoveTo end = (MoveTo) endCommand; + MoveTo start = (MoveTo) startCommand; + if (start != null) { + path.moveTo(AndroidUtilities.dp(start.x + (end.x - start.x) * progress), AndroidUtilities.dp(start.y + (end.y - start.y) * progress)); + } else { + path.moveTo(AndroidUtilities.dp(end.x), AndroidUtilities.dp(end.y)); + } + } else if (endCommand instanceof LineTo) { + LineTo end = (LineTo) endCommand; + LineTo start = (LineTo) startCommand; + if (start != null) { + path.lineTo(AndroidUtilities.dp(start.x + (end.x - start.x) * progress), AndroidUtilities.dp(start.y + (end.y - start.y) * progress)); + } else { + path.lineTo(AndroidUtilities.dp(end.x), AndroidUtilities.dp(end.y)); + } + } else if (endCommand instanceof CurveTo) { + CurveTo end = (CurveTo) endCommand; + CurveTo start = (CurveTo) startCommand; + if (start != null) { + path.cubicTo(AndroidUtilities.dp(start.x1 + (end.x1 - start.x1) * progress), AndroidUtilities.dp(start.y1 + (end.y1 - start.y1) * progress), + AndroidUtilities.dp(start.x2 + (end.x2 - start.x2) * progress), AndroidUtilities.dp(start.y2 + (end.y2 - start.y2) * progress), + AndroidUtilities.dp(start.x + (end.x - start.x) * progress), AndroidUtilities.dp(start.y + (end.y - start.y) * progress)); + } else { + path.cubicTo(AndroidUtilities.dp(end.x1), AndroidUtilities.dp(end.y1), AndroidUtilities.dp(end.x2), AndroidUtilities.dp(end.y2), AndroidUtilities.dp(end.x), AndroidUtilities.dp(end.y)); + } + } + } + path.close(); + canvas.drawPath(path, paint); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoCropView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoCropView.java index 71f52deb0..d4af7401d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoCropView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoCropView.java @@ -8,33 +8,93 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; import android.os.Build; +import android.util.Property; import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.OvershootInterpolator; import android.widget.FrameLayout; import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.MediaController; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.BubbleActivity; import org.telegram.ui.Components.Crop.CropRotationWheel; +import org.telegram.ui.Components.Crop.CropTransform; import org.telegram.ui.Components.Crop.CropView; public class PhotoCropView extends FrameLayout { public interface PhotoCropViewDelegate { void onChange(boolean reset); + void onUpdate(); + void onTapUp(); + int getVideoThumbX(); + void onVideoThumbClick(); } private PhotoCropViewDelegate delegate; - private boolean showOnSetBitmap; private CropView cropView; private CropRotationWheel wheelView; + private boolean inBubbleMode; + + private ImageReceiver thumbImageView; + private boolean thumbImageVisible; + private boolean thumbImageVisibleOverride = true; + private float thumbImageVisibleProgress; + private float thumbAnimationProgress = 1.0f; + private AnimatorSet thumbAnimation; + private AnimatorSet thumbOverrideAnimation; + private float flashAlpha = 0.0f; + + private Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + public final Property ANIMATION_VALUE = new AnimationProperties.FloatProperty("thumbAnimationProgress") { + @Override + public void setValue(PhotoCropView object, float value) { + thumbAnimationProgress = value; + object.invalidate(); + } + + @Override + public Float get(PhotoCropView object) { + return thumbAnimationProgress; + } + }; + + public final Property PROGRESS_VALUE = new AnimationProperties.FloatProperty("thumbImageVisibleProgress") { + @Override + public void setValue(PhotoCropView object, float value) { + thumbImageVisibleProgress = value; + object.invalidate(); + } + + @Override + public Float get(PhotoCropView object) { + return thumbImageVisibleProgress; + } + }; + public PhotoCropView(Context context) { super(context); - cropView = new CropView(getContext()); + inBubbleMode = context instanceof BubbleActivity; + + cropView = new CropView(context); cropView.setListener(new CropView.CropViewListener() { @Override public void onChange(boolean reset) { @@ -43,15 +103,31 @@ public class PhotoCropView extends FrameLayout { } } + @Override + public void onUpdate() { + if (delegate != null) { + delegate.onUpdate(); + } + } + @Override public void onAspectLock(boolean enabled) { wheelView.setAspectLock(enabled); } + + @Override + public void onTapUp() { + if (delegate != null) { + delegate.onTapUp(); + } + } }); cropView.setBottomPadding(AndroidUtilities.dp(64)); addView(cropView); - wheelView = new CropRotationWheel(getContext()); + thumbImageView = new ImageReceiver(this); + + wheelView = new CropRotationWheel(context); wheelView.setListener(new CropRotationWheel.RotationWheelListener() { @Override public void onStart() { @@ -77,41 +153,167 @@ public class PhotoCropView extends FrameLayout { } @Override - public void rotate90Pressed() { - rotate(); + public boolean rotate90Pressed() { + return rotate(); + } + + @Override + public boolean mirror() { + return PhotoCropView.this.mirror(); } }); addView(wheelView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER | Gravity.BOTTOM, 0, 0, 0, 0)); } - public void rotate() { - if (wheelView != null) { - wheelView.reset(); + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (thumbImageVisibleOverride && thumbImageVisible && thumbImageView.isInsideImage(event.getX(), event.getY())) { + if (event.getAction() == MotionEvent.ACTION_UP) { + delegate.onVideoThumbClick(); + } + return true; } - cropView.rotate90Degrees(); + return super.onInterceptTouchEvent(event); } - public void setBitmap(Bitmap bitmap, int rotation, boolean freeform, boolean update, PaintingOverlay paintingOverlay) { + @Override + public boolean onTouchEvent(MotionEvent event) { + if (thumbImageVisibleOverride && thumbImageVisible && thumbImageView.isInsideImage(event.getX(), event.getY())) { + if (event.getAction() == MotionEvent.ACTION_UP) { + delegate.onVideoThumbClick(); + } + return true; + } + return super.onTouchEvent(event); + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean result = super.drawChild(canvas, child, drawingTime); + if (thumbImageVisible && child == cropView) { + RectF rect = cropView.getActualRect(); + int targetSize = AndroidUtilities.dp(32); + int targetX = delegate.getVideoThumbX() - targetSize / 2 + AndroidUtilities.dp(2); + int targetY = getMeasuredHeight() - AndroidUtilities.dp(156); + float x = rect.left + (targetX - rect.left) * thumbAnimationProgress; + float y = rect.top + (targetY - rect.top) * thumbAnimationProgress; + float size = rect.width() + (targetSize - rect.width()) * thumbAnimationProgress; + thumbImageView.setRoundRadius((int) (size / 2)); + thumbImageView.setImageCoords(x, y, size, size); + thumbImageView.setAlpha(thumbImageVisibleProgress); + thumbImageView.draw(canvas); + + if (flashAlpha > 0.0f) { + circlePaint.setColor(0xffffffff); + circlePaint.setAlpha((int) (flashAlpha * 255)); + canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2, circlePaint); + } + + circlePaint.setColor(Theme.getColor(Theme.key_dialogFloatingButton)); + circlePaint.setAlpha(Math.min(255, (int) (255 * thumbAnimationProgress * thumbImageVisibleProgress))); + canvas.drawCircle(targetX + targetSize / 2, targetY + targetSize + AndroidUtilities.dp(8), AndroidUtilities.dp(3), circlePaint); + } + return result; + } + + public boolean rotate() { + if (wheelView != null) { + wheelView.reset(false); + } + return cropView.rotate90Degrees(); + } + + public boolean mirror() { + return cropView.mirror(); + } + + public void setBitmap(Bitmap bitmap, int rotation, boolean freeform, boolean update, PaintingOverlay paintingOverlay, CropTransform cropTransform, VideoEditTextureView videoView, MediaController.CropState state) { requestLayout(); - cropView.setBitmap(bitmap, rotation, freeform, update, paintingOverlay); - - if (showOnSetBitmap) { - showOnSetBitmap = false; - cropView.show(); - } - + thumbImageVisible = false; + thumbImageView.setImageBitmap((Drawable) null); + cropView.setBitmap(bitmap, rotation, freeform, update, paintingOverlay, cropTransform, videoView, state); wheelView.setFreeform(freeform); - wheelView.reset(); + wheelView.reset(true); + if (state != null) { + wheelView.setRotation(state.cropRotate, false); + wheelView.setRotated(state.transformRotation != 0); + wheelView.setMirrored(state.mirrored); + } else { + wheelView.setRotated(false); + wheelView.setMirrored(false); + } wheelView.setVisibility(freeform ? VISIBLE : INVISIBLE); } + public void setVideoThumbFlashAlpha(float alpha) { + flashAlpha = alpha; + invalidate(); + } + + public Bitmap getVideoThumb() { + return thumbImageVisible && thumbImageVisibleOverride ? thumbImageView.getBitmap() : null; + } + + public void setVideoThumb(Bitmap bitmap, int orientation) { + thumbImageVisible = bitmap != null; + thumbImageView.setImageBitmap(bitmap); + thumbImageView.setOrientation(orientation, false); + if (thumbAnimation != null) { + thumbAnimation.cancel(); + } + if (thumbOverrideAnimation != null) { + thumbOverrideAnimation.cancel(); + } + thumbImageVisibleOverride = true; + thumbImageVisibleProgress = 1.0f; + thumbAnimation = new AnimatorSet(); + thumbAnimation.playTogether(ObjectAnimator.ofFloat(this, ANIMATION_VALUE, 0.0f, 1.0f)); + thumbAnimation.setDuration(250); + thumbAnimation.setInterpolator(new OvershootInterpolator(1.01f)); + thumbAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + thumbAnimation = null; + } + }); + thumbAnimation.start(); + } + + public void cancelThumbAnimation() { + if (thumbAnimation != null) { + thumbAnimation.cancel(); + thumbAnimation = null; + thumbImageVisible = false; + } + } + + public void setVideoThumbVisible(boolean visible) { + if (thumbImageVisibleOverride == visible) { + return; + } + thumbImageVisibleOverride = visible; + if (thumbOverrideAnimation != null) { + thumbOverrideAnimation.cancel(); + } + thumbOverrideAnimation = new AnimatorSet(); + thumbOverrideAnimation.playTogether(ObjectAnimator.ofFloat(this, PROGRESS_VALUE, visible ? 1.0f : 0.0f)); + thumbOverrideAnimation.setDuration(180); + thumbOverrideAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + thumbOverrideAnimation = null; + } + }); + thumbOverrideAnimation.start(); + } + public boolean isReady() { return cropView.isReady(); } public void reset() { - wheelView.reset(); + wheelView.reset(true); cropView.reset(); } @@ -123,30 +325,24 @@ public class PhotoCropView extends FrameLayout { cropView.setAspectRatio(ratio); } - public void hideBackView() { - cropView.hideBackView(); - } - - public void showBackView() { - cropView.showBackView(); - } - public void setFreeform(boolean freeform) { cropView.setFreeform(freeform); } public void onAppeared() { - if (cropView != null) { - cropView.show(); - } else { - showOnSetBitmap = true; - } + cropView.show(); } public void onDisappear() { - if (cropView != null) { - cropView.hide(); - } + cropView.hide(); + } + + public void onShow() { + cropView.onShow(); + } + + public void onHide() { + cropView.onHide(); } public float getRectX() { @@ -154,7 +350,7 @@ public class PhotoCropView extends FrameLayout { } public float getRectY() { - return cropView.getCropTop() - AndroidUtilities.dp(14) - (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + return cropView.getCropTop() - AndroidUtilities.dp(14) - (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); } public float getRectSizeX() { @@ -165,12 +361,12 @@ public class PhotoCropView extends FrameLayout { return cropView.getCropHeight(); } - public Bitmap getBitmap(MediaController.MediaEditState editState) { - return cropView.getResult(editState); + public void makeCrop(MediaController.MediaEditState editState) { + cropView.makeCrop(editState); } - public void setDelegate(PhotoCropViewDelegate delegate) { - this.delegate = delegate; + public void setDelegate(PhotoCropViewDelegate photoCropViewDelegate) { + delegate = photoCropViewDelegate; } @Override @@ -180,4 +376,12 @@ public class PhotoCropView extends FrameLayout { cropView.updateLayout(); } } + + @Override + public void invalidate() { + super.invalidate(); + if (cropView != null) { + cropView.invalidate(); + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterBlurControl.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterBlurControl.java index 3c834fc0d..0129f9eb4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterBlurControl.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterBlurControl.java @@ -17,6 +17,7 @@ import android.view.MotionEvent; import android.widget.FrameLayout; import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.BubbleActivity; public class PhotoFilterBlurControl extends FrameLayout { @@ -70,6 +71,8 @@ public class PhotoFilterBlurControl extends FrameLayout { private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private Paint arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private boolean inBubbleMode; + private PhotoFilterLinearBlurControlDelegate delegate; public PhotoFilterBlurControl(Context context) { @@ -79,6 +82,8 @@ public class PhotoFilterBlurControl extends FrameLayout { arcPaint.setColor(0xffffffff); arcPaint.setStrokeWidth(AndroidUtilities.dp(2)); arcPaint.setStyle(Paint.Style.STROKE); + + inBubbleMode = context instanceof BubbleActivity; } public void setType(int blurType) { @@ -256,7 +261,7 @@ public class PhotoFilterBlurControl extends FrameLayout { case BlurViewActiveControlCenter: { float translationX = locationX - pointerStartX; float translationY = locationY - pointerStartY; - Rect actualArea = new Rect((getWidth() - actualAreaSize.width) / 2, (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2, actualAreaSize.width, actualAreaSize.height); + Rect actualArea = new Rect((getWidth() - actualAreaSize.width) / 2, (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2, actualAreaSize.width, actualAreaSize.height); Point newPoint = new Point(Math.max(actualArea.x, Math.min(actualArea.x + actualArea.width, startCenterPoint.x + translationX)), Math.max(actualArea.y, Math.min(actualArea.y + actualArea.height, startCenterPoint.y + translationY))); centerPoint = new Point((newPoint.x - actualArea.x) / actualAreaSize.width, ((newPoint.y - actualArea.y) + (actualAreaSize.width - actualAreaSize.height) / 2) / actualAreaSize.width); } @@ -342,7 +347,7 @@ public class PhotoFilterBlurControl extends FrameLayout { case BlurViewActiveControlCenter: { float translationX = locationX - pointerStartX; float translationY = locationY - pointerStartY; - Rect actualArea = new Rect((getWidth() - actualAreaSize.width) / 2, (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2, actualAreaSize.width, actualAreaSize.height); + Rect actualArea = new Rect((getWidth() - actualAreaSize.width) / 2, (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2, actualAreaSize.width, actualAreaSize.height); Point newPoint = new Point(Math.max(actualArea.x, Math.min(actualArea.x + actualArea.width, startCenterPoint.x + translationX)), Math.max(actualArea.y, Math.min(actualArea.y + actualArea.height, startCenterPoint.y + translationY))); centerPoint = new Point((newPoint.x - actualArea.x) / actualAreaSize.width, ((newPoint.y - actualArea.y) + (actualAreaSize.width - actualAreaSize.height) / 2) / actualAreaSize.width); } @@ -494,7 +499,7 @@ public class PhotoFilterBlurControl extends FrameLayout { } private Point getActualCenterPoint() { - return new Point((getWidth() - actualAreaSize.width) / 2 + centerPoint.x * actualAreaSize.width, (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2 - (actualAreaSize.width - actualAreaSize.height) / 2 + centerPoint.y * actualAreaSize.width); + return new Point((getWidth() - actualAreaSize.width) / 2 + centerPoint.x * actualAreaSize.width, (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0) + (getHeight() - actualAreaSize.height) / 2 - (actualAreaSize.width - actualAreaSize.height) / 2 + centerPoint.y * actualAreaSize.width); } private float getActualInnerRadius() { 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 4c14c3b99..0541c584f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java @@ -33,6 +33,7 @@ import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; import org.telegram.messenger.R; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.BubbleActivity; import org.telegram.ui.Cells.PhotoEditRadioCell; import org.telegram.ui.Cells.PhotoEditToolCell; @@ -57,6 +58,7 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter private int saturationTool; private int warmthTool; private int fadeTool; + private int softenSkinTool; private int highlightsTool; private int shadowsTool; private int vignetteTool; @@ -72,6 +74,7 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter private float warmthValue; //-100 100 private float saturationValue; //-100 100 private float fadeValue; // 0 100 + private float softenSkinValue; // 0 100 private int tintShadowsColor; //0 0xffffffff private int tintHighlightsColor; //0 0xffffffff private float highlightsValue; //-100 100 @@ -104,6 +107,9 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter private FrameLayout curveLayout; private RadioButton[] curveRadioButton = new RadioButton[4]; private PaintingOverlay paintingOverlay; + private boolean isMirrored; + + private boolean inBubbleMode; private int selectedTool; @@ -263,12 +269,19 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter } } - public PhotoFilterView(Context context, VideoEditTextureView videoTextureView, Bitmap bitmap, int rotation, MediaController.SavedFilterState state, PaintingOverlay overlay) { + public PhotoFilterView(Context context, VideoEditTextureView videoTextureView, Bitmap bitmap, int rotation, MediaController.SavedFilterState state, PaintingOverlay overlay, int hasFaces, boolean mirror) { super(context); + inBubbleMode = context instanceof BubbleActivity; paintingOverlay = overlay; + isMirrored = mirror; rowsCount = 0; + if (hasFaces == 1) { + softenSkinTool = rowsCount++; + } else if (hasFaces == 0) { + softenSkinTool = -1; + } enhanceTool = rowsCount++; exposureTool = rowsCount++; contrastTool = rowsCount++; @@ -278,18 +291,21 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter highlightsTool = rowsCount++; shadowsTool = rowsCount++; vignetteTool = rowsCount++; + if (hasFaces == 2) { + softenSkinTool = rowsCount++; + } if (videoTextureView == null) { grainTool = rowsCount++; - sharpenTool = rowsCount++; } else { grainTool = -1; - sharpenTool = -1; } + sharpenTool = rowsCount++; tintShadowsTool = rowsCount++; tintHighlightsTool = rowsCount++; if (state != null) { enhanceValue = state.enhanceValue; + softenSkinValue = state.softenSkinValue; exposureValue = state.exposureValue; contrastValue = state.contrastValue; warmthValue = state.warmthValue; @@ -334,7 +350,7 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { if (eglThread == null && surface != null) { - eglThread = new FilterGLThread(surface, bitmapToEdit, orientation); + eglThread = new FilterGLThread(surface, bitmapToEdit, orientation, isMirrored); eglThread.setFilterGLThreadDelegate(PhotoFilterView.this); eglThread.setSurfaceTextureSize(width, height); eglThread.requestRender(true, true, false); @@ -580,7 +596,7 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter updateSelectedBlurType(); - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= 21 && !inBubbleMode) { if (ownsTextureView) { ((LayoutParams) textureView.getLayoutParams()).topMargin = AndroidUtilities.statusBarHeight; } @@ -649,6 +665,7 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter state.warmthValue = warmthValue; state.saturationValue = saturationValue; state.fadeValue = fadeValue; + state.softenSkinValue = softenSkinValue; state.tintShadowsColor = tintShadowsColor; state.tintHighlightsColor = tintHighlightsColor; state.highlightsValue = highlightsValue; @@ -678,12 +695,13 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter grainValue != lastState.grainValue || sharpenValue != lastState.sharpenValue || fadeValue != lastState.fadeValue || + softenSkinValue != lastState.softenSkinValue || tintHighlightsColor != lastState.tintHighlightsColor || tintShadowsColor != lastState.tintShadowsColor || !curvesToolValue.shouldBeSkipped(); } else { return enhanceValue != 0 || contrastValue != 0 || highlightsValue != 0 || exposureValue != 0 || warmthValue != 0 || saturationValue != 0 || vignetteValue != 0 || - shadowsValue != 0 || grainValue != 0 || sharpenValue != 0 || fadeValue != 0 || tintHighlightsColor != 0 || tintShadowsColor != 0 || !curvesToolValue.shouldBeSkipped(); + shadowsValue != 0 || grainValue != 0 || sharpenValue != 0 || fadeValue != 0 || softenSkinValue != 0 || tintHighlightsColor != 0 || tintShadowsColor != 0 || !curvesToolValue.shouldBeSkipped(); } } @@ -774,7 +792,7 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter private void fixLayout(int viewWidth, int viewHeight) { viewWidth -= AndroidUtilities.dp(28); - viewHeight -= AndroidUtilities.dp(14 + 140 + 60) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + viewHeight -= AndroidUtilities.dp(14 + 140 + 60) + (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); float bitmapW; float bitmapH; @@ -801,7 +819,7 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter } int bitmapX = (int) Math.ceil((viewWidth - bitmapW) / 2 + AndroidUtilities.dp(14)); - int bitmapY = (int) Math.ceil((viewHeight - bitmapH) / 2 + AndroidUtilities.dp(14) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0)); + int bitmapY = (int) Math.ceil((viewHeight - bitmapH) / 2 + AndroidUtilities.dp(14) + (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0)); int width = (int) bitmapW; int height = (int) bitmapH; @@ -812,7 +830,7 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter layoutParams.width = width; layoutParams.height = height; } - curvesControl.setActualArea(bitmapX, bitmapY - (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0), width, height); + curvesControl.setActualArea(bitmapX, bitmapY - (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0), width, height); blurControl.setActualAreaSize(width, height); LayoutParams layoutParams = (LayoutParams) blurControl.getLayoutParams(); @@ -904,6 +922,11 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter return fadeValue / 100.0f; } + @Override + public float getSoftenSkinValue() { + return softenSkinValue / 100.0f; + } + @Override public float getTintHighlightsIntensityValue() { float tintHighlightsIntensity = 50.0f; @@ -1041,8 +1064,10 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter grainValue = progress; } else if (i1 == sharpenTool) { sharpenValue = progress; - } else if (i1 == fadeTool) { + } else if (i1 == fadeTool) { fadeValue = progress; + } else if (i1 == softenSkinTool) { + softenSkinValue = progress; } if (eglThread != null) { eglThread.requestRender(true); @@ -1099,6 +1124,8 @@ public class PhotoFilterView extends FrameLayout implements FilterShaders.Filter cell.setIconAndTextAndValue(LocaleController.getString("Sharpen", R.string.Sharpen), sharpenValue, 0, 100); } else if (i == fadeTool) { cell.setIconAndTextAndValue(LocaleController.getString("Fade", R.string.Fade), fadeValue, 0, 100); + } else if (i == softenSkinTool) { + cell.setIconAndTextAndValue(LocaleController.getString("SoftenSkin", R.string.SoftenSkin), softenSkinValue, 0, 100); } break; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoPaintView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoPaintView.java index ebb843995..0f0e77d56 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoPaintView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoPaintView.java @@ -33,6 +33,7 @@ import org.telegram.messenger.DispatchQueue; import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MediaController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.R; import org.telegram.messenger.VideoEditedInfo; @@ -41,6 +42,7 @@ import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarPopupWindow; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.BubbleActivity; import org.telegram.ui.Components.Paint.Brush; import org.telegram.ui.Components.Paint.Painting; import org.telegram.ui.Components.Paint.RenderView; @@ -67,7 +69,8 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView private Brush[] brushes = new Brush[]{ new Brush.Radial(), new Brush.Elliptical(), - new Brush.Neon() + new Brush.Neon(), + new Brush.Arrow() }; private FrameLayout toolsView; @@ -82,6 +85,10 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView private FrameLayout selectionContainerView; private ColorPicker colorPicker; + private float transformX; + private float transformY; + private float[] temp = new float[2]; + private ImageView paintButton; private EntityView currentEntityView; @@ -117,9 +124,15 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView private int originalBitmapRotation; - public PhotoPaintView(Context context, Bitmap bitmap, Bitmap originalBitmap, int originalRotation, ArrayList entities, Runnable onInit) { + private boolean inBubbleMode; + + private MediaController.CropState currentCropState; + + public PhotoPaintView(Context context, Bitmap bitmap, Bitmap originalBitmap, int originalRotation, ArrayList entities, MediaController.CropState cropState, Runnable onInit) { super(context); + inBubbleMode = context instanceof BubbleActivity; + currentCropState = cropState; queue = new DispatchQueue("Paint"); originalBitmapRotation = originalRotation; @@ -355,8 +368,12 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView } } - float x = renderView.getMeasuredWidth() / 2 + (ev.getX() - renderView.getTranslationX() - getMeasuredWidth() / 2) / renderView.getScaleX(); - float y = renderView.getMeasuredHeight() / 2 + (ev.getY() - renderView.getTranslationY() - getMeasuredHeight() / 2 + AndroidUtilities.dp(32)) / renderView.getScaleY(); + float x2 = (ev.getX() - renderView.getTranslationX() - getMeasuredWidth() / 2) / renderView.getScaleX(); + float y2 = (ev.getY() - renderView.getTranslationY() - getMeasuredHeight() / 2 + AndroidUtilities.dp(32)) / renderView.getScaleY(); + float rotation = (float) Math.toRadians(-renderView.getRotation()); + float x = (float) (x2 * Math.cos(rotation) - y2 * Math.sin(rotation)) + renderView.getMeasuredWidth() / 2; + float y = (float) (x2 * Math.sin(rotation) + y2 * Math.cos(rotation)) + renderView.getMeasuredHeight() / 2; + MotionEvent event = MotionEvent.obtain(0, 0, ev.getActionMasked(), x, y, 0); renderView.onTouch(event); event.recycle(); @@ -449,7 +466,7 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView return colorPicker; } - private boolean hasChanges() { + public boolean hasChanges() { return undoStore.canUndo(); } @@ -572,9 +589,9 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView return; } AlertDialog.Builder builder = new AlertDialog.Builder(parentActivity); - builder.setMessage(LocaleController.getString("DiscardChanges", R.string.DiscardChanges)); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); - builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), (dialogInterface, i) -> okRunnable.run()); + builder.setMessage(LocaleController.getString("PhotoEditorDiscardAlert", R.string.PhotoEditorDiscardAlert)); + builder.setTitle(LocaleController.getString("DiscardChanges", R.string.DiscardChanges)); + builder.setPositiveButton(LocaleController.getString("PassportDiscard", R.string.PassportDiscard), (dialogInterface, i) -> okRunnable.run()); builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); photoViewer.showAlertDialog(builder); } else { @@ -701,14 +718,14 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView int width = right - left; int height = bottom - top; - int status = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + int status = (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); int actionBarHeight = ActionBar.getCurrentActionBarHeight(); - int actionBarHeight2 = ActionBar.getCurrentActionBarHeight() + status; + int actionBarHeight2 = actionBarHeight + status; int maxHeight = AndroidUtilities.displaySize.y - actionBarHeight - AndroidUtilities.dp(48); int x = (int) Math.ceil((width - renderView.getMeasuredWidth()) / 2); - int y = actionBarHeight2 + (height - actionBarHeight2 - AndroidUtilities.dp(48) - renderView.getMeasuredHeight()) / 2 - ActionBar.getCurrentActionBarHeight() + AndroidUtilities.dp(8); + int y = (height - actionBarHeight2 - AndroidUtilities.dp(48) - renderView.getMeasuredHeight()) / 2 + AndroidUtilities.dp(8) + status; renderView.layout(x, y, x + renderView.getMeasuredWidth(), y + renderView.getMeasuredHeight()); int x2 = x + (renderView.getMeasuredWidth() - entitiesView.getMeasuredWidth()) / 2; @@ -740,24 +757,88 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView return true; } + @Override + public float[] getTransformedTouch(float x, float y) { + float x2 = (x - AndroidUtilities.displaySize.x / 2); + float y2 = (y - AndroidUtilities.displaySize.y / 2); + float rotation = (float) Math.toRadians(-entitiesView.getRotation()); + temp[0] = (float) (x2 * Math.cos(rotation) - y2 * Math.sin(rotation)) + AndroidUtilities.displaySize.x / 2; + temp[1] = (float) (x2 * Math.sin(rotation) + y2 * Math.cos(rotation)) + AndroidUtilities.displaySize.y / 2; + return temp; + } + + @Override + public int[] getCenterLocation(EntityView entityView) { + return getCenterLocationInWindow(entityView); + } + @Override public boolean allowInteraction(EntityView entityView) { return !editingText; } + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + boolean restore = false; + if ((child == renderView || child == entitiesView || child == selectionContainerView) && currentCropState != null) { + canvas.save(); + + int status = (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); + int actionBarHeight = ActionBar.getCurrentActionBarHeight(); + int actionBarHeight2 = actionBarHeight + status; + + int vw = child.getMeasuredWidth(); + int vh = child.getMeasuredHeight(); + int tr = currentCropState.transformRotation; + if (tr == 90 || tr == 270) { + int temp = vw; + vw = vh; + vh = temp; + } + + int w = (int) (vw * currentCropState.cropPw * child.getScaleX() / currentCropState.cropScale); + int h = (int) (vh * currentCropState.cropPh * child.getScaleY() / currentCropState.cropScale); + float x = (float) Math.ceil((getMeasuredWidth() - w) / 2) + transformX; + float y = (getMeasuredHeight() - actionBarHeight2 - AndroidUtilities.dp(48) - h) / 2 + AndroidUtilities.dp(8) + status + transformY; + + canvas.clipRect(Math.max(0, x), Math.max(0, y), Math.min(x + w, getMeasuredWidth()), Math.min(getMeasuredHeight(), y + h)); + restore = true; + } + boolean result = super.drawChild(canvas, child, drawingTime); + if (restore) { + canvas.restore(); + } + return result; + } + private Point centerPositionForEntity() { Size paintingSize = getPaintingSize(); - return new Point(paintingSize.width / 2.0f, paintingSize.height / 2.0f); + float x = paintingSize.width / 2.0f; + float y = paintingSize.height / 2.0f; + if (currentCropState != null) { + float rotation = (float) Math.toRadians(-(currentCropState.transformRotation + currentCropState.cropRotate)); + float px = (float) (currentCropState.cropPx * Math.cos(rotation) - currentCropState.cropPy * Math.sin(rotation)); + float py = (float) (currentCropState.cropPx * Math.sin(rotation) + currentCropState.cropPy * Math.cos(rotation)); + x -= px * paintingSize.width; + y -= py * paintingSize.height; + } + return new Point(x, y); } private Point startPositionRelativeToEntity(EntityView entityView) { - final float offset = 200.0f; + float offset = 200.0f; + if (currentCropState != null) { + offset /= currentCropState.cropScale; + } if (entityView != null) { Point position = entityView.getPosition(); return new Point(position.x + offset, position.y + offset); } else { - final float minimalDistance = 100.0f; + float minimalDistance = 100.0f; + if (currentCropState != null) { + minimalDistance /= currentCropState.cropScale; + } Point position = centerPositionForEntity(); while (true) { boolean occupied = false; @@ -806,25 +887,61 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView return result; } - public void setTransform(float scale, float trX, float trY) { + public void setTransform(float scale, float trX, float trY, float imageWidth, float imageHeight) { + transformX = trX; + transformY = trY; for (int a = 0; a < 3; a++) { View view; + float additionlScale = 1.0f; if (a == 0) { view = entitiesView; - view.setScaleX(baseScale * scale); - view.setScaleY(baseScale * scale); } else { if (a == 1) { view = selectionContainerView; } else { view = renderView; } - view.setScaleX(scale); - view.setScaleY(scale); } - view.setTranslationX(trX); - view.setTranslationY(trY); + float tx; + float ty; + float rotation = 0; + if (currentCropState != null) { + additionlScale *= currentCropState.cropScale; + + int w = view.getMeasuredWidth(); + int h = view.getMeasuredHeight(); + int tr = currentCropState.transformRotation; + int fw = w, rotatedW = w; + int fh = h, rotatedH = h; + if (tr == 90 || tr == 270) { + int temp = fw; + fw = rotatedW = fh; + fh = rotatedH = temp; + } + fw *= currentCropState.cropPw; + fh *= currentCropState.cropPh; + + float sc = Math.max(imageWidth / fw, imageHeight / fh); + additionlScale *= sc; + + tx = trX + currentCropState.cropPx * rotatedW * scale * sc * currentCropState.cropScale; + ty = trY + currentCropState.cropPy * rotatedH * scale * sc * currentCropState.cropScale; + rotation = currentCropState.cropRotate + tr; + } else { + if (a == 0) { + additionlScale *= baseScale; + } + tx = trX; + ty = trY; + } + view.setScaleX(scale * additionlScale); + view.setScaleY(scale * additionlScale); + view.setTranslationX(tx); + view.setTranslationY(ty); + view.setRotation(rotation); + view.invalidate(); } + invalidate(); } private boolean selectEntity(EntityView entityView) { @@ -930,7 +1047,12 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView private StickerView createSticker(Object parentObject, TLRPC.Document sticker, boolean select) { StickerPosition position = calculateStickerPosition(sticker); - StickerView view = new StickerView(getContext(), position.position, position.angle, position.scale, baseStickerSize(), sticker, parentObject); + StickerView view = new StickerView(getContext(), position.position, position.angle, position.scale, baseStickerSize(), sticker, parentObject) { + @Override + protected void didSetAnimatedSticker(RLottieDrawable drawable) { + PhotoPaintView.this.didSetAnimatedSticker(drawable); + } + }; view.setDelegate(this); entitiesView.addView(view); if (select) { @@ -940,16 +1062,16 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView return view; } + protected void didSetAnimatedSticker(RLottieDrawable drawable) { + + } + private void mirrorSticker() { if (currentEntityView instanceof StickerView) { ((StickerView) currentEntityView).mirror(); } } - private int baseFontSize() { - return (int) (getPaintingSize().width / 9); - } - private TextPaintView createText(boolean select) { Swatch currentSwatch = colorPicker.getSwatch(); Swatch swatch; @@ -961,10 +1083,15 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView swatch = new Swatch(Color.WHITE, 1.0f, currentSwatch.brushWeight); } - TextPaintView view = new TextPaintView(getContext(), startPositionRelativeToEntity(null), baseFontSize(), "", swatch, selectedTextType); + Size paintingSize = getPaintingSize(); + TextPaintView view = new TextPaintView(getContext(), startPositionRelativeToEntity(null), (int) (paintingSize.width / 9), "", swatch, selectedTextType); view.setDelegate(this); - view.setMaxWidth((int) (getPaintingSize().width - 20)); + view.setMaxWidth((int) (paintingSize.width - 20)); entitiesView.addView(view, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT)); + if (currentCropState != null) { + view.scale(1.0f / currentCropState.cropScale); + view.rotate(-(currentCropState.transformRotation + currentCropState.cropRotate)); + } if (select) { registerRemovalUndo(view); @@ -991,8 +1118,13 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView editedTextScale = textPaintView.getScale(); textPaintView.setPosition(centerPositionForEntity()); - textPaintView.setRotation(0.0f); - textPaintView.setScale(1.0f); + if (currentCropState != null) { + textPaintView.setRotation(-(currentCropState.transformRotation + currentCropState.cropRotate)); + textPaintView.setScale(1.0f / currentCropState.cropScale); + } else { + textPaintView.setRotation(0.0f); + textPaintView.setScale(1.0f); + } toolsView.setVisibility(GONE); @@ -1061,9 +1193,29 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView } } + private int[] pos = new int[2]; + + private int[] getCenterLocationInWindow(View view) { + view.getLocationInWindow(pos); + float rotation = (float) Math.toRadians(view.getRotation() + (currentCropState != null ? currentCropState.cropRotate + currentCropState.transformRotation : 0)); + float width = view.getWidth() * view.getScaleX() * entitiesView.getScaleX(); + float height = view.getHeight() * view.getScaleY() * entitiesView.getScaleY(); + float px = (float) (width * Math.cos(rotation) - height * Math.sin(rotation)); + float py = (float) (width * Math.sin(rotation) + height * Math.cos(rotation)); + pos[0] += px / 2; + pos[1] += py / 2; + return pos; + } + + @Override + public float getCropRotation() { + return currentCropState != null ? currentCropState.cropRotate + currentCropState.transformRotation : 0; + } + private void showMenuForEntity(final EntityView entityView) { - int x = (int) ((entityView.getPosition().x - entitiesView.getWidth() / 2) * entitiesView.getScaleX()); - int y = (int) ((entityView.getPosition().y - entityView.getHeight() * entityView.getScale() / 2 - entitiesView.getHeight() / 2) * entitiesView.getScaleY()) - AndroidUtilities.dp(32); + int[] pos = getCenterLocationInWindow(entityView); + int x = pos[0]; + int y = pos[1] - AndroidUtilities.dp(32); showPopup(() -> { LinearLayout parent = new LinearLayout(getContext()); @@ -1128,11 +1280,17 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView params.width = LayoutHelper.WRAP_CONTENT; params.height = LayoutHelper.WRAP_CONTENT; parent.setLayoutParams(params); - }, entityView, Gravity.CENTER, x, y); + }, this, Gravity.LEFT | Gravity.TOP, x, y); } - private FrameLayout buttonForBrush(final int brush, int resource, boolean applyColor, boolean selected) { - FrameLayout button = new FrameLayout(getContext()); + private LinearLayout buttonForBrush(final int brush, int icon, String text, boolean selected) { + LinearLayout button = new LinearLayout(getContext()) { + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return true; + } + }; + button.setOrientation(LinearLayout.HORIZONTAL); button.setBackgroundDrawable(Theme.getSelectorDrawable(false)); button.setOnClickListener(v -> { setBrush(brush); @@ -1142,49 +1300,43 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView } }); - ImageView preview = new ImageView(getContext()); - preview.setImageResource(resource); - if (applyColor) { - preview.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem), PorterDuff.Mode.SRC_IN)); - } - button.addView(preview, LayoutHelper.createFrame(165, 44, Gravity.LEFT | Gravity.CENTER_VERTICAL, 46, 0, 8, 0)); + ImageView imageView = new ImageView(getContext()); + imageView.setScaleType(ImageView.ScaleType.CENTER); + imageView.setImageResource(icon); + imageView.setColorFilter(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem)); + button.addView(imageView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 16, 0, 16, 0)); - if (selected) { - ImageView check = new ImageView(getContext()); - check.setImageResource(R.drawable.ic_ab_done); - check.setScaleType(ImageView.ScaleType.CENTER); - check.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.SRC_IN)); - button.addView(check, LayoutHelper.createFrame(50, LayoutHelper.MATCH_PARENT)); - } + TextView textView = new TextView(getContext()); + textView.setTextColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); + textView.setText(text); + textView.setMinWidth(AndroidUtilities.dp(70)); + button.addView(textView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 0, 0, 16, 0)); + + ImageView check = new ImageView(getContext()); + check.setImageResource(R.drawable.msg_text_check); + check.setScaleType(ImageView.ScaleType.CENTER); + check.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_radioBackgroundChecked), PorterDuff.Mode.SRC_IN)); + check.setVisibility(selected ? VISIBLE : INVISIBLE); + button.addView(check, LayoutHelper.createLinear(50, LayoutHelper.MATCH_PARENT)); return button; } private void showBrushSettings() { showPopup(() -> { - View radial = buttonForBrush(0, R.drawable.paint_radial_preview, true, currentBrush == 0); - popupLayout.addView(radial); + View radial = buttonForBrush(0, R.drawable.msg_draw_pen, LocaleController.getString("PaintPen", R.string.PaintPen), currentBrush == 0); + popupLayout.addView(radial, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 54)); - LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) radial.getLayoutParams(); - layoutParams.width = LayoutHelper.MATCH_PARENT; - layoutParams.height = AndroidUtilities.dp(52); - radial.setLayoutParams(layoutParams); + View elliptical = buttonForBrush(1, R.drawable.msg_draw_marker, LocaleController.getString("PaintMarker", R.string.PaintMarker), currentBrush == 1); + popupLayout.addView(elliptical, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 54)); - View elliptical = buttonForBrush(1, R.drawable.paint_elliptical_preview, true, currentBrush == 1); - popupLayout.addView(elliptical); + View neon = buttonForBrush(2, R.drawable.msg_draw_neon, LocaleController.getString("PaintNeon", R.string.PaintNeon), currentBrush == 2); + popupLayout.addView(neon, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 54)); - layoutParams = (LinearLayout.LayoutParams) elliptical.getLayoutParams(); - layoutParams.width = LayoutHelper.MATCH_PARENT; - layoutParams.height = AndroidUtilities.dp(52); - elliptical.setLayoutParams(layoutParams); + View arrow = buttonForBrush(3, R.drawable.msg_draw_arrow, LocaleController.getString("PaintArrow", R.string.PaintArrow), currentBrush == 3); + popupLayout.addView(arrow, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 54)); - View neon = buttonForBrush(2, R.drawable.paint_neon_preview, false, currentBrush == 2); - popupLayout.addView(neon); - - layoutParams = (LinearLayout.LayoutParams) neon.getLayoutParams(); - layoutParams.width = LayoutHelper.MATCH_PARENT; - layoutParams.height = AndroidUtilities.dp(52); - neon.setLayoutParams(layoutParams); }, this, Gravity.RIGHT | Gravity.BOTTOM, 0, AndroidUtilities.dp(48)); } @@ -1222,7 +1374,7 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView check.setImageResource(R.drawable.msg_text_check); check.setScaleType(ImageView.ScaleType.CENTER); check.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_radioBackgroundChecked), PorterDuff.Mode.SRC_IN)); - button.addView(check, LayoutHelper.createFrame(50, LayoutHelper.MATCH_PARENT)); + button.addView(check, LayoutHelper.createLinear(50, LayoutHelper.MATCH_PARENT)); } return button; @@ -1296,6 +1448,10 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView popupWindow.setFocusable(true); + if ((gravity & Gravity.TOP) != 0) { + x -= popupLayout.getMeasuredWidth() / 2; + y -= popupLayout.getMeasuredHeight(); + } popupWindow.showAtLocation(parent, gravity, x, y); popupWindow.startAnimation(); } @@ -1381,7 +1537,16 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView } } - StickerPosition defaultPosition = new StickerPosition(centerPositionForEntity(), 0.75f, 0.0f); + float rotation; + float baseScale; + if (currentCropState != null) { + rotation = -(currentCropState.transformRotation + currentCropState.cropRotate); + baseScale = 0.75f / currentCropState.cropScale; + } else { + rotation = 0.0f; + baseScale = 0.75f; + } + StickerPosition defaultPosition = new StickerPosition(centerPositionForEntity(), baseScale, rotation); /* if (maskCoords == null || faces == null || faces.size() == 0) { return defaultPosition; @@ -1390,8 +1555,8 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView PhotoFace face = getRandomFaceWithVacantAnchor(anchor, document.id, maskCoords); if (face == null) {*/ - return defaultPosition; - }/* + return defaultPosition; + }/* Point referencePoint = face.getPointForAnchor(anchor); float referenceWidth = face.getWidthForAnchor(anchor); @@ -1463,6 +1628,7 @@ public class PhotoPaintView extends FrameLayout implements EntityView.EntityView return false; } */ + private static class StickerPosition { private Point position; private float scale; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PipVideoView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PipVideoView.java index 1b57d5085..23ca619e6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PipVideoView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PipVideoView.java @@ -66,7 +66,6 @@ public class PipVideoView { private Paint progressInnerPaint; private boolean isVisible = true; private AnimatorSet currentAnimation; - private ImageView inlineButton; private ImageView playButton; private float progress; private boolean isCompleted; @@ -96,7 +95,7 @@ public class PipVideoView { public MiniControlsView(Context context, boolean fullControls) { super(context); - inlineButton = new ImageView(context); + ImageView inlineButton = new ImageView(context); inlineButton.setScaleType(ImageView.ScaleType.CENTER); inlineButton.setImageResource(R.drawable.ic_outinline); addView(inlineButton, LayoutHelper.createFrame(56, 48, Gravity.RIGHT | Gravity.TOP)); @@ -135,7 +134,7 @@ public class PipVideoView { }); } - //setOnTouchListener((v, event) -> true); + setOnTouchListener((v, event) -> true); updatePlayButton(); show(false, false); } @@ -294,7 +293,7 @@ public class PipVideoView { private boolean dragging; @Override - public boolean onInterceptTouchEvent(MotionEvent event) { + public boolean dispatchTouchEvent(MotionEvent event) { float x = event.getRawX(); float y = event.getRawY(); if (event.getAction() == MotionEvent.ACTION_DOWN) { @@ -311,50 +310,50 @@ public class PipVideoView { return true; } } - return super.onInterceptTouchEvent(event); + + if (dragging) { + if (event.getAction() == MotionEvent.ACTION_MOVE) { + float dx = (x - startX); + float dy = (y - startY); + windowLayoutParams.x += dx; + windowLayoutParams.y += dy; + int maxDiff = videoWidth / 2; + if (windowLayoutParams.x < -maxDiff) { + windowLayoutParams.x = -maxDiff; + } else if (windowLayoutParams.x > AndroidUtilities.displaySize.x - windowLayoutParams.width + maxDiff) { + windowLayoutParams.x = AndroidUtilities.displaySize.x - windowLayoutParams.width + maxDiff; + } + float alpha = 1.0f; + if (windowLayoutParams.x < 0) { + alpha = 1.0f + windowLayoutParams.x / (float) maxDiff * 0.5f; + } else if (windowLayoutParams.x > AndroidUtilities.displaySize.x - windowLayoutParams.width) { + alpha = 1.0f - (windowLayoutParams.x - AndroidUtilities.displaySize.x + windowLayoutParams.width) / (float) maxDiff * 0.5f; + } + if (windowView.getAlpha() != alpha) { + windowView.setAlpha(alpha); + } + maxDiff = 0; + if (windowLayoutParams.y < -maxDiff) { + windowLayoutParams.y = -maxDiff; + } else if (windowLayoutParams.y > AndroidUtilities.displaySize.y - windowLayoutParams.height + maxDiff) { + windowLayoutParams.y = AndroidUtilities.displaySize.y - windowLayoutParams.height + maxDiff; + } + windowManager.updateViewLayout(windowView, windowLayoutParams); + startX = x; + startY = y; + } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { + dragging = false; + animateToBoundsMaybe(); + } + return true; + } else { + return super.dispatchTouchEvent(event); + } } @Override - public boolean onTouchEvent(MotionEvent event) { - if (!dragging) { - return false; - } - float x = event.getRawX(); - float y = event.getRawY(); - if (event.getAction() == MotionEvent.ACTION_MOVE) { - float dx = (x - startX); - float dy = (y - startY); - windowLayoutParams.x += dx; - windowLayoutParams.y += dy; - int maxDiff = videoWidth / 2; - if (windowLayoutParams.x < -maxDiff) { - windowLayoutParams.x = -maxDiff; - } else if (windowLayoutParams.x > AndroidUtilities.displaySize.x - windowLayoutParams.width + maxDiff) { - windowLayoutParams.x = AndroidUtilities.displaySize.x - windowLayoutParams.width + maxDiff; - } - float alpha = 1.0f; - if (windowLayoutParams.x < 0) { - alpha = 1.0f + windowLayoutParams.x / (float) maxDiff * 0.5f; - } else if (windowLayoutParams.x > AndroidUtilities.displaySize.x - windowLayoutParams.width) { - alpha = 1.0f - (windowLayoutParams.x - AndroidUtilities.displaySize.x + windowLayoutParams.width) / (float) maxDiff * 0.5f; - } - if (windowView.getAlpha() != alpha) { - windowView.setAlpha(alpha); - } - maxDiff = 0; - if (windowLayoutParams.y < -maxDiff) { - windowLayoutParams.y = -maxDiff; - } else if (windowLayoutParams.y > AndroidUtilities.displaySize.y - windowLayoutParams.height + maxDiff) { - windowLayoutParams.y = AndroidUtilities.displaySize.y - windowLayoutParams.height + maxDiff; - } - windowManager.updateViewLayout(windowView, windowLayoutParams); - startX = x; - startY = y; - } else if (event.getAction() == MotionEvent.ACTION_UP) { - dragging = false; - animateToBoundsMaybe(); - } - return super.onTouchEvent(event); + public boolean onInterceptTouchEvent(MotionEvent event) { + return super.onInterceptTouchEvent(event); } }; @@ -420,7 +419,7 @@ public class PipVideoView { windowLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; } } - windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; windowManager.addView(windowView, windowLayoutParams); } catch (Exception e) { FileLog.e(e); @@ -462,7 +461,7 @@ public class PipVideoView { if (isX) { total = AndroidUtilities.displaySize.x - sideSize; } else { - total = AndroidUtilities.displaySize.y - sideSize - ActionBar.getCurrentActionBarHeight(); + total = AndroidUtilities.displaySize.y - sideSize - (ActionBar.getCurrentActionBarHeight() + AndroidUtilities.statusBarHeight); } int result; if (side == 0) { @@ -473,7 +472,7 @@ public class PipVideoView { result = Math.round((total - AndroidUtilities.dp(20)) * p) + AndroidUtilities.dp(10); } if (!isX) { - result += ActionBar.getCurrentActionBarHeight(); + result += ActionBar.getCurrentActionBarHeight() + AndroidUtilities.statusBarHeight; } return result; } @@ -514,7 +513,7 @@ public class PipVideoView { } editor.putInt("sidex", 0); if (windowView.getAlpha() != 1.0f) { - animators.add(ObjectAnimator.ofFloat(windowView, "alpha", 1.0f)); + animators.add(ObjectAnimator.ofFloat(windowView, View.ALPHA, 1.0f)); } animators.add(ObjectAnimator.ofInt(this, "x", startX)); } else if (Math.abs(endX - windowLayoutParams.x) <= maxDiff || windowLayoutParams.x > AndroidUtilities.displaySize.x - videoWidth && windowLayoutParams.x < AndroidUtilities.displaySize.x - videoWidth / 4 * 3) { @@ -523,7 +522,7 @@ public class PipVideoView { } editor.putInt("sidex", 1); if (windowView.getAlpha() != 1.0f) { - animators.add(ObjectAnimator.ofFloat(windowView, "alpha", 1.0f)); + animators.add(ObjectAnimator.ofFloat(windowView, View.ALPHA, 1.0f)); } animators.add(ObjectAnimator.ofInt(this, "x", endX)); } else if (windowView.getAlpha() != 1.0f) { @@ -541,7 +540,7 @@ public class PipVideoView { editor.putInt("sidex", 2); } if (!slideOut) { - if (Math.abs(startY - windowLayoutParams.y) <= maxDiff || windowLayoutParams.y <= ActionBar.getCurrentActionBarHeight()) { + if (Math.abs(startY - windowLayoutParams.y) <= maxDiff || windowLayoutParams.y <= (ActionBar.getCurrentActionBarHeight() + AndroidUtilities.statusBarHeight)) { if (animators == null) { animators = new ArrayList<>(); } @@ -567,7 +566,7 @@ public class PipVideoView { animatorSet.setInterpolator(decelerateInterpolator); animatorSet.setDuration(150); if (slideOut) { - animators.add(ObjectAnimator.ofFloat(windowView, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(windowView, View.ALPHA, 0.0f)); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -625,4 +624,26 @@ public class PipVideoView { windowLayoutParams.y = value; windowManager.updateViewLayout(windowView, windowLayoutParams); } + + @Keep + public int getWidth() { + return windowLayoutParams.width; + } + + @Keep + public int getHeight() { + return windowLayoutParams.height; + } + + @Keep + public void setWidth(int value) { + windowLayoutParams.width = videoWidth = value; + windowManager.updateViewLayout(windowView, windowLayoutParams); + } + + @Keep + public void setHeight(int value) { + windowLayoutParams.height = videoHeight = value; + windowManager.updateViewLayout(windowView, windowLayoutParams); + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ProfileGalleryView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ProfileGalleryView.java index f1ade92e9..fc106b11d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ProfileGalleryView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ProfileGalleryView.java @@ -8,6 +8,8 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; @@ -16,7 +18,9 @@ import android.view.ViewGroup; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.FileLoader; +import org.telegram.messenger.ImageLoader; import org.telegram.messenger.ImageLocation; +import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.UserConfig; @@ -40,6 +44,7 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio private final ViewPagerAdapter adapter; private final int parentClassGuid; private final long dialogId; + private TLRPC.ChatFull chatInfo; private boolean scrolledByUser; private boolean isDownReleased; @@ -47,13 +52,20 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio private int currentAccount = UserConfig.selectedAccount; private ImageLocation prevImageLocation; + private ArrayList videoFileNames = new ArrayList<>(); private ArrayList thumbsFileNames = new ArrayList<>(); + private ArrayList photos = new ArrayList<>(); + private ArrayList videoLocations = new ArrayList<>(); private ArrayList imagesLocations = new ArrayList<>(); private ArrayList thumbsLocations = new ArrayList<>(); private ArrayList imagesLocationsSizes = new ArrayList<>(); + private int settingMainPhoto; + private final SparseArray radialProgresses = new SparseArray<>(); + private OnPageChangeListener onPageChangeListener; + private static class Item { private BackupImageView imageView; } @@ -61,6 +73,8 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio public interface Callback { void onDown(boolean left); void onRelease(); + void onPhotosLoaded(); + void onVideoSet(); } public ProfileGalleryView(Context context, long dialogId, ActionBar parentActionBar, RecyclerListView parentListView, ProfileActivity.AvatarImageView parentAvatarImageView, int parentClassGuid, Callback callback) { @@ -76,9 +90,63 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio this.touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); this.callback = callback; + addOnPageChangeListener(new OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (positionOffsetPixels == 0) { + position = adapter.getRealPosition(position); + BackupImageView currentView = getCurrentItemView(); + int count = getChildCount(); + for (int a = 0; a < count; a++) { + View child = getChildAt(a); + if (!(child instanceof BackupImageView)) { + continue; + } + int p = adapter.getRealPosition(adapter.imageViews.indexOf(child)); + BackupImageView imageView = (BackupImageView) child; + ImageReceiver imageReceiver = imageView.getImageReceiver(); + boolean currentAllow = imageReceiver.getAllowStartAnimation(); + if (p == position) { + if (!currentAllow) { + imageReceiver.setAllowStartAnimation(true); + imageReceiver.startAnimation(); + } + ImageLocation location = videoLocations.get(p); + if (location != null) { + FileLoader.getInstance(currentAccount).setForceStreamLoadingFile(location.location, "mp4"); + } + } else { + if (currentAllow) { + AnimatedFileDrawable fileDrawable = imageReceiver.getAnimation(); + if (fileDrawable != null) { + ImageLocation location = videoLocations.get(p); + if (location != null) { + fileDrawable.seekTo(location.videoSeekTo, false, true); + } + } + imageReceiver.setAllowStartAnimation(false); + imageReceiver.stopAnimation(); + } + } + } + } + } + + @Override + public void onPageSelected(int position) { + + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + }); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.dialogPhotosLoaded); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.fileDidLoad); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.FileLoadProgressChanged); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.reloadDialogPhotos); MessagesController.getInstance(currentAccount).loadDialogPhotos((int) dialogId, 80, 0, true, parentClassGuid); } @@ -86,6 +154,50 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.dialogPhotosLoaded); NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.fileDidLoad); NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.FileLoadProgressChanged); + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.reloadDialogPhotos); + int count = getChildCount(); + for (int a = 0; a < count; a++) { + View child = getChildAt(a); + if (!(child instanceof BackupImageView)) { + continue; + } + BackupImageView imageView = (BackupImageView) child; + if (imageView.getImageReceiver().hasStaticThumb()) { + Drawable drawable = imageView.getImageReceiver().getDrawable(); + if (drawable instanceof AnimatedFileDrawable) { + ((AnimatedFileDrawable) drawable).removeSecondParentView(imageView); + } + } + } + } + + public void setAnimatedFileMaybe(AnimatedFileDrawable drawable) { + if (drawable == null) { + return; + } + int count = getChildCount(); + for (int a = 0; a < count; a++) { + View child = getChildAt(a); + if (!(child instanceof BackupImageView)) { + continue; + } + int p = adapter.getRealPosition(adapter.imageViews.indexOf(child)); + if (p != 0) { + continue; + } + BackupImageView imageView = (BackupImageView) child; + ImageReceiver imageReceiver = imageView.getImageReceiver(); + AnimatedFileDrawable currentDrawable = imageReceiver.getAnimation(); + if (currentDrawable == drawable) { + continue; + } + if (currentDrawable != null) { + currentDrawable.removeSecondParentView(imageView); + } + imageView.setImageDrawable(drawable); + drawable.addSecondParentView(this); + drawable.setInvalidateParentViewWithSecond(true); + } } @Override @@ -176,26 +288,58 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio return result; } - public void initIfEmpty(ImageLocation imageLocation, ImageLocation thumbLocation) { - if (imageLocation == null || thumbLocation == null) { - return; + public void setChatInfo(TLRPC.ChatFull chatFull) { + chatInfo = chatFull; + if (!photos.isEmpty() && photos.get(0) == null && chatInfo != null && FileLoader.isSamePhoto(imagesLocations.get(0).location, chatInfo.chat_photo)) { + photos.set(0, chatInfo.chat_photo); + if (!chatInfo.chat_photo.video_sizes.isEmpty()) { + final TLRPC.VideoSize videoSize = chatInfo.chat_photo.video_sizes.get(0); + videoLocations.set(0, ImageLocation.getForPhoto(videoSize, chatInfo.chat_photo)); + videoFileNames.set(0, FileLoader.getAttachFileName(videoSize)); + callback.onPhotosLoaded(); + } else { + videoLocations.set(0, null); + videoFileNames.add(0, null); + } + adapter.notifyDataSetChanged(); } - if (prevImageLocation != null && prevImageLocation.location.local_id != imageLocation.location.local_id) { + } + + public boolean initIfEmpty(ImageLocation imageLocation, ImageLocation thumbLocation) { + if (imageLocation == null || thumbLocation == null || settingMainPhoto != 0) { + return false; + } + if (prevImageLocation == null && imageLocation != null || prevImageLocation != null && prevImageLocation.location.local_id != imageLocation.location.local_id) { imagesLocations.clear(); MessagesController.getInstance(currentAccount).loadDialogPhotos((int) dialogId, 80, 0, true, parentClassGuid); } if (!imagesLocations.isEmpty()) { - return; + return false; } prevImageLocation = imageLocation; - thumbsFileNames.add(""); + thumbsFileNames.add(null); + videoFileNames.add(null); imagesLocations.add(imageLocation); thumbsLocations.add(thumbLocation); + videoLocations.add(null); + photos.add(null); imagesLocationsSizes.add(-1); getAdapter().notifyDataSetChanged(); + return true; } public ImageLocation getImageLocation(int index) { + if (index < 0 || index >= imagesLocations.size()) { + return null; + } + ImageLocation location = videoLocations.get(index); + if (location != null) { + return location; + } + return imagesLocations.get(index); + } + + public ImageLocation getRealImageLocation(int index) { if (index < 0 || index >= imagesLocations.size()) { return null; } @@ -221,18 +365,151 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio } } + public boolean isLoadingCurrentVideo() { + if (videoLocations.get(getRealPosition()) == null) { + return false; + } + BackupImageView imageView = getCurrentItemView(); + if (imageView == null) { + return false; + } + AnimatedFileDrawable drawable = imageView.getImageReceiver().getAnimation(); + return drawable == null || !drawable.hasBitmap(); + } + + public float getCurrentItemProgress() { + BackupImageView imageView = getCurrentItemView(); + if (imageView == null) { + return 0.0f; + } + AnimatedFileDrawable drawable = imageView.getImageReceiver().getAnimation(); + if (drawable == null) { + return 0.0f; + } + return drawable.getCurrentProgress(); + } + + public boolean isCurrentItemVideo() { + return videoLocations.get(getRealPosition()) != null; + } + + public ImageLocation getCurrentVideoLocation(ImageLocation thumbLocation, ImageLocation imageLocation) { + if (thumbLocation == null) { + return null; + } + for (int b = 0; b < 2; b++) { + ArrayList arrayList = b == 0 ? thumbsLocations : imagesLocations; + for (int a = 0, N = arrayList.size(); a < N; a++) { + ImageLocation loc = arrayList.get(a); + if (loc == null || loc.location == null) { + continue; + } + if (loc.dc_id == thumbLocation.dc_id && loc.location.local_id == thumbLocation.location.local_id && loc.location.volume_id == thumbLocation.location.volume_id || + loc.dc_id == imageLocation.dc_id && loc.location.local_id == imageLocation.location.local_id && loc.location.volume_id == imageLocation.location.volume_id) { + return videoLocations.get(a); + } + } + } + + return null; + } + public void resetCurrentItem() { setCurrentItem(adapter.getExtraCount(), false); } public int getRealCount() { - return adapter.getCount() - adapter.getExtraCount() * 2; + return photos.size(); + } + + public int getRealPosition(int position) { + return adapter.getRealPosition(position); } public int getRealPosition() { return adapter.getRealPosition(getCurrentItem()); } + public TLRPC.Photo getPhoto(int index) { + if (index < 0 || index >= photos.size()) { + return null; + } + return photos.get(index); + } + + public void replaceFirstPhoto(TLRPC.Photo oldPhoto, TLRPC.Photo photo) { + if (photos.isEmpty()) { + return; + } + int idx = photos.indexOf(oldPhoto); + if (idx < 0) { + return; + } + photos.set(idx, photo); + } + + public void finishSettingMainPhoto() { + settingMainPhoto--; + } + + public void startMovePhotoToBegin(int index) { + if (index <= 0 || index >= photos.size()) { + return; + } + settingMainPhoto++; + TLRPC.Photo photo = photos.get(index); + photos.remove(index); + photos.add(0, photo); + + String name = thumbsFileNames.get(index); + thumbsFileNames.remove(index); + thumbsFileNames.add(0, name); + + videoFileNames.add(0, videoFileNames.remove(index)); + + ImageLocation location = videoLocations.get(index); + videoLocations.remove(index); + videoLocations.add(0, location); + + location = imagesLocations.get(index); + imagesLocations.remove(index); + imagesLocations.add(0, location); + + location = thumbsLocations.get(index); + thumbsLocations.remove(index); + thumbsLocations.add(0, location); + + Integer size = imagesLocationsSizes.get(index); + imagesLocationsSizes.remove(index); + imagesLocationsSizes.add(0, size); + + prevImageLocation = imagesLocations.get(0); + } + + public void commitMoveToBegin() { + adapter.notifyDataSetChanged(); + resetCurrentItem(); + } + + public boolean removePhotoAtIndex(int index) { + if (index < 0 || index >= photos.size()) { + return false; + } + photos.remove(index); + thumbsFileNames.remove(index); + videoFileNames.remove(index); + videoLocations.remove(index); + imagesLocations.remove(index); + thumbsLocations.remove(index); + imagesLocationsSizes.remove(index); + radialProgresses.delete(index); + if (index == 0 && !imagesLocations.isEmpty()) { + prevImageLocation = imagesLocations.get(0); + } + adapter.notifyDataSetChanged(); + return photos.isEmpty(); + } + @Override public boolean onInterceptTouchEvent(MotionEvent e) { if (parentListView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { @@ -262,34 +539,62 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio int did = (Integer) args[0]; if (did == dialogId && parentClassGuid == guid) { boolean fromCache = (Boolean) args[2]; - ArrayList photos = (ArrayList) args[4]; + ArrayList arrayList = (ArrayList) args[4]; thumbsFileNames.clear(); + videoFileNames.clear(); imagesLocations.clear(); + videoLocations.clear(); thumbsLocations.clear(); + photos.clear(); imagesLocationsSizes.clear(); ImageLocation currentImageLocation = null; if (did < 0) { TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(-did); currentImageLocation = ImageLocation.getForChat(chat, true); if (currentImageLocation != null) { - thumbsFileNames.add(""); imagesLocations.add(currentImageLocation); thumbsLocations.add(ImageLocation.getForChat(chat, false)); + thumbsFileNames.add(null); + if (chatInfo != null && FileLoader.isSamePhoto(currentImageLocation.location, chatInfo.chat_photo)) { + photos.add(chatInfo.chat_photo); + if (!chatInfo.chat_photo.video_sizes.isEmpty()) { + final TLRPC.VideoSize videoSize = chatInfo.chat_photo.video_sizes.get(0); + videoLocations.add(ImageLocation.getForPhoto(videoSize, chatInfo.chat_photo)); + videoFileNames.add(FileLoader.getAttachFileName(videoSize)); + } else { + videoLocations.add(null); + videoFileNames.add(null); + } + } else { + photos.add(null); + videoFileNames.add(null); + videoLocations.add(null); + } imagesLocationsSizes.add(-1); } } - for (int a = 0; a < photos.size(); a++) { - TLRPC.Photo photo = photos.get(a); + for (int a = 0; a < arrayList.size(); a++) { + TLRPC.Photo photo = arrayList.get(a); if (photo == null || photo instanceof TLRPC.TL_photoEmpty || photo.sizes == null) { continue; } - TLRPC.PhotoSize sizeFull = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 640); TLRPC.PhotoSize sizeThumb = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 50); + for (int b = 0, N = photo.sizes.size(); b < N; b++) { + TLRPC.PhotoSize photoSize = photo.sizes.get(b); + if (photoSize instanceof TLRPC.TL_photoStrippedSize) { + sizeThumb = photoSize; + break; + } + } if (currentImageLocation != null) { boolean cont = false; - for (int b = 0; b < photo.sizes.size(); b++) { + for (int b = 0, N = photo.sizes.size(); b < N; b++) { TLRPC.PhotoSize size = photo.sizes.get(b); - if (size.location.local_id == currentImageLocation.location.local_id && size.location.volume_id == currentImageLocation.location.volume_id) { + if (size.location != null && size.location.local_id == currentImageLocation.location.local_id && size.location.volume_id == currentImageLocation.location.volume_id) { + photos.set(0, photo); + if (!photo.video_sizes.isEmpty()) { + videoLocations.set(0, ImageLocation.getForPhoto(photo.video_sizes.get(0), photo)); + } cont = true; break; } @@ -298,6 +603,7 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio continue; } } + TLRPC.PhotoSize sizeFull = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 640); if (sizeFull != null) { if (photo.dc_id != 0) { sizeFull.location.dc_id = photo.dc_id; @@ -306,8 +612,17 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio ImageLocation location = ImageLocation.getForPhoto(sizeFull, photo); if (location != null) { imagesLocations.add(location); - thumbsFileNames.add(FileLoader.getAttachFileName(sizeThumb)); + thumbsFileNames.add(FileLoader.getAttachFileName(sizeThumb instanceof TLRPC.TL_photoStrippedSize ? sizeFull : sizeThumb)); thumbsLocations.add(ImageLocation.getForPhoto(sizeThumb, photo)); + if (!photo.video_sizes.isEmpty()) { + final TLRPC.VideoSize videoSize = photo.video_sizes.get(0); + videoLocations.add(ImageLocation.getForPhoto(videoSize, photo)); + videoFileNames.add(FileLoader.getAttachFileName(videoSize)); + } else { + videoLocations.add(null); + videoFileNames.add(null); + } + photos.add(photo); imagesLocationsSizes.add(sizeFull.size); } } @@ -320,11 +635,18 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio if (fromCache) { MessagesController.getInstance(currentAccount).loadDialogPhotos(did, 80, 0, false, parentClassGuid); } + if (callback != null) { + callback.onPhotosLoaded(); + } } } else if (id == NotificationCenter.fileDidLoad) { final String fileName = (String) args[0]; for (int i = 0; i < thumbsFileNames.size(); i++) { - if (thumbsFileNames.get(i).equals(fileName)) { + String fileName2 = videoFileNames.get(i); + if (fileName2 == null) { + fileName2 = thumbsFileNames.get(i); + } + if (fileName2 != null && TextUtils.equals(fileName, fileName2)) { final RadialProgress2 radialProgress = radialProgresses.get(i); if (radialProgress != null) { radialProgress.setProgress(1f, true); @@ -334,7 +656,11 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio } else if (id == NotificationCenter.FileLoadProgressChanged) { String fileName = (String) args[0]; for (int i = 0; i < thumbsFileNames.size(); i++) { - if (thumbsFileNames.get(i).equals(fileName)) { + String fileName2 = videoFileNames.get(i); + if (fileName2 == null) { + fileName2 = thumbsFileNames.get(i); + } + if (fileName2 != null && TextUtils.equals(fileName, fileName2)) { final RadialProgress2 radialProgress = radialProgresses.get(i); if (radialProgress != null) { Long loadedSize = (Long) args[1]; @@ -344,12 +670,18 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio } } } + } else if (id == NotificationCenter.reloadDialogPhotos) { + if (settingMainPhoto != 0) { + return; + } + MessagesController.getInstance(currentAccount).loadDialogPhotos((int) dialogId, 80, 0, true, parentClassGuid); } } public class ViewPagerAdapter extends Adapter { private final ArrayList objects = new ArrayList<>(); + private final ArrayList imageViews = new ArrayList<>(); private final Context context; private final Paint placeholderPaint; @@ -376,7 +708,7 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio @Override public int getItemPosition(Object object) { - final int idx = objects.indexOf(object); + final int idx = objects.indexOf((Item) object); return idx == -1 ? POSITION_NONE : idx; } @@ -393,19 +725,61 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio private ValueAnimator radialProgressHideAnimator; private float radialProgressHideAnimatorStartValue; private long firstDrawTime = -1; + private boolean isVideo; { + getImageReceiver().setAllowDecodeSingleFrame(true); final int realPosition = getRealPosition(position); + boolean needProgress = false; if (realPosition == 0) { - setImage(imagesLocations.get(realPosition), null, parentAvatarImageView.getImageReceiver().getBitmap(), imagesLocationsSizes.get(realPosition), 1, null); + Drawable drawable = parentAvatarImageView.getImageReceiver().getDrawable(); + if (drawable instanceof AnimatedFileDrawable && ((AnimatedFileDrawable) drawable).hasBitmap()) { + AnimatedFileDrawable animatedFileDrawable = (AnimatedFileDrawable) drawable; + setImageDrawable(drawable); + animatedFileDrawable.addSecondParentView(this); + animatedFileDrawable.setInvalidateParentViewWithSecond(true); + } else { + ImageLocation videoLocation = videoLocations.get(realPosition); + isVideo = videoLocation != null; + needProgress = true; + String filter; + if (videoLocation != null && videoLocation.imageType == FileLoader.IMAGE_TYPE_ANIMATION) { + filter = ImageLoader.AUTOPLAY_FILTER; + } else { + filter = null; + } + setImageMedia(videoLocations.get(realPosition), filter, imagesLocations.get(realPosition), null, parentAvatarImageView.getImageReceiver().getBitmap(), imagesLocationsSizes.get(realPosition), 1, null); + } } else { - setImage(imagesLocations.get(realPosition), null, thumbsLocations.get(realPosition), null, null, 0, 1, imagesLocationsSizes.get(realPosition)); - radialProgress = new RadialProgress2(this); - radialProgress.setOverrideAlpha(0.0f); - radialProgress.setIcon(MediaActionDrawable.ICON_EMPTY, false, false); - radialProgress.setColors(0x42000000, 0x42000000, Color.WHITE, Color.WHITE); - radialProgresses.append(position, radialProgress); + final ImageLocation videoLocation = videoLocations.get(realPosition); + isVideo = videoLocation != null; + needProgress = true; + ImageLocation location = thumbsLocations.get(realPosition); + String filter = location.photoSize instanceof TLRPC.TL_photoStrippedSize ? "b" : null; + setImageMedia(videoLocation, null, imagesLocations.get(realPosition), null, thumbsLocations.get(realPosition), filter, null, 0, 1, imagesLocationsSizes.get(realPosition)); } + if (needProgress) { + radialProgress = radialProgresses.get(realPosition); + if (radialProgress == null) { + radialProgress = new RadialProgress2(this); + radialProgress.setOverrideAlpha(0.0f); + radialProgress.setIcon(MediaActionDrawable.ICON_EMPTY, false, false); + radialProgress.setColors(0x42000000, 0x42000000, Color.WHITE, Color.WHITE); + radialProgresses.append(realPosition, radialProgress); + } + postInvalidateOnAnimation(); + } + getImageReceiver().setDelegate(new ImageReceiver.ImageReceiverDelegate() { + @Override + public void didSetImage(ImageReceiver imageReceiver, boolean set, boolean thumb, boolean memCache) { + + } + + @Override + public void onAnimationReady(ImageReceiver imageReceiver) { + callback.onVideoSet(); + } + }); getImageReceiver().setCrossfadeAlpha((byte) 2); } @@ -422,18 +796,25 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio @Override protected void onDraw(Canvas canvas) { if (radialProgress != null) { - if (getImageReceiver().getDrawable() != null) { + final Drawable drawable = getImageReceiver().getDrawable(); + if (drawable != null && (!isVideo || (drawable instanceof AnimatedFileDrawable && ((AnimatedFileDrawable) drawable).getDurationMs() > 0))) { if (radialProgressHideAnimator == null) { + long startDelay = 0; + if (radialProgress.getProgress() < 1f) { + radialProgress.setProgress(1f, true); + startDelay = 100; + } radialProgressHideAnimatorStartValue = radialProgress.getOverrideAlpha(); radialProgressHideAnimator = ValueAnimator.ofFloat(0f, 1f); - radialProgressHideAnimator.setDuration((long) (radialProgressHideAnimatorStartValue * 175f)); + radialProgressHideAnimator.setStartDelay(startDelay); + radialProgressHideAnimator.setDuration((long) (radialProgressHideAnimatorStartValue * 250f)); radialProgressHideAnimator.setInterpolator(CubicBezierInterpolator.DEFAULT); radialProgressHideAnimator.addUpdateListener(anim -> radialProgress.setOverrideAlpha(AndroidUtilities.lerp(radialProgressHideAnimatorStartValue, 0f, anim.getAnimatedFraction()))); radialProgressHideAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { radialProgress = null; - radialProgresses.delete(position); + radialProgresses.delete(getRealPosition(position)); } }); radialProgressHideAnimator.start(); @@ -443,9 +824,11 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio firstDrawTime = System.currentTimeMillis(); } else { final long elapsedTime = System.currentTimeMillis() - firstDrawTime; - if (elapsedTime <= 1000) { - if (elapsedTime > 750) { - radialProgress.setOverrideAlpha(CubicBezierInterpolator.DEFAULT.getInterpolation((elapsedTime - 750) / 250f)); + final long startDelay = isVideo ? 250 : 750; + final long duration = 250; + if (elapsedTime <= startDelay + duration) { + if (elapsedTime > startDelay) { + radialProgress.setOverrideAlpha(CubicBezierInterpolator.DEFAULT.getInterpolation((elapsedTime - startDelay) / (float) duration)); } } } @@ -461,6 +844,7 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio } } }; + imageViews.set(position, item.imageView); } if (item.imageView.getParent() == null) { @@ -472,8 +856,14 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio @Override public void destroyItem(ViewGroup container, int position, Object object) { - container.removeView(((Item) object).imageView); - radialProgresses.delete(getRealPosition(position)); + BackupImageView imageView = ((Item) object).imageView; + if (imageView.getImageReceiver().hasStaticThumb()) { + Drawable drawable = imageView.getImageReceiver().getDrawable(); + if (drawable instanceof AnimatedFileDrawable) { + ((AnimatedFileDrawable) drawable).removeSecondParentView(imageView); + } + } + container.removeView(imageView); } @Nullable @@ -487,6 +877,7 @@ public class ProfileGalleryView extends CircularViewPager implements Notificatio objects.clear(); for (int a = 0, N = imagesLocations.size() + getExtraCount() * 2; a < N; a++) { objects.add(new Item()); + imageViews.add(null); } super.notifyDataSetChanged(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RLottieDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RLottieDrawable.java index 6b8a3e9bf..41f7f2bad 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RLottieDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RLottieDrawable.java @@ -27,6 +27,7 @@ import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.DispatchQueuePool; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; +import org.telegram.messenger.Utilities; import java.io.File; import java.io.FileInputStream; @@ -104,6 +105,9 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { private volatile boolean isRecycled; private volatile long nativePtr; private volatile long secondNativePtr; + private boolean loadingInBackground; + private boolean secondLoadingInBackground; + private boolean destroyAfterLoading; private int secondFramesCount; private volatile boolean setLastFrame; @@ -405,28 +409,39 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { } public boolean setBaseDice(File path) { - if (nativePtr != 0) { + if (nativePtr != 0 || loadingInBackground) { return true; } String jsonString = readRes(path, 0); if (TextUtils.isEmpty(jsonString)) { return false; } - nativePtr = createWithJson(jsonString, "dice", metaData, null); - timeBetweenFrames = Math.max(16, (int) (1000.0f / metaData[1])); - if (isRunning) { - scheduleNextGetFrame(); - invalidateInternal(); - } + loadingInBackground = true; + Utilities.globalQueue.postRunnable(() -> { + nativePtr = createWithJson(jsonString, "dice", metaData, null); + AndroidUtilities.runOnUIThread(() -> { + loadingInBackground = false; + if (!secondLoadingInBackground && destroyAfterLoading) { + recycle(); + return; + } + timeBetweenFrames = Math.max(16, (int) (1000.0f / metaData[1])); + if (isRunning) { + scheduleNextGetFrame(); + invalidateInternal(); + } + }); + }); + return true; } public boolean hasBaseDice() { - return nativePtr != 0; + return nativePtr != 0 || loadingInBackground; } public boolean setDiceNumber(File path, boolean instant) { - if (secondNativePtr != 0) { + if (secondNativePtr != 0 || secondLoadingInBackground) { return true; } String jsonString = readRes(path, 0); @@ -437,10 +452,33 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { isDice = 2; setLastFrame = true; } - int[] metaData2 = new int[3]; - secondNativePtr = createWithJson(jsonString, "dice", metaData2, null); - secondFramesCount = metaData2[0]; - timeBetweenFrames = Math.max(16, (int) (1000.0f / metaData2[1])); + secondLoadingInBackground = true; + Utilities.globalQueue.postRunnable(() -> { + if (destroyAfterLoading) { + AndroidUtilities.runOnUIThread(() -> { + secondLoadingInBackground = false; + if (!loadingInBackground && destroyAfterLoading) { + recycle(); + } + }); + return; + } + int[] metaData2 = new int[3]; + secondNativePtr = createWithJson(jsonString, "dice", metaData2, null); + AndroidUtilities.runOnUIThread(() -> { + secondLoadingInBackground = false; + if (!secondLoadingInBackground && destroyAfterLoading) { + recycle(); + return; + } + secondFramesCount = metaData2[0]; + timeBetweenFrames = Math.max(16, (int) (1000.0f / metaData2[1])); + if (isRunning) { + scheduleNextGetFrame(); + invalidateInternal(); + } + }); + }); return true; } @@ -493,7 +531,6 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { } } } catch (Throwable e) { - FileLog.e(e); return null; } finally { try { @@ -601,7 +638,9 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { isRunning = false; isRecycled = true; checkRunningTasks(); - if (loadFrameTask == null && cacheGenerateTask == null) { + if (loadingInBackground || secondLoadingInBackground) { + destroyAfterLoading = true; + } else if (loadFrameTask == null && cacheGenerateTask == null) { if (nativePtr != 0) { destroy(nativePtr); nativePtr = 0; @@ -714,7 +753,7 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { } private boolean scheduleNextGetFrame() { - if (loadFrameTask != null || nextRenderingBitmap != null || nativePtr == 0 || destroyWhenDone || !isRunning && (!decodeSingleFrame || decodeSingleFrame && singleFrameDecoded)) { + if (loadFrameTask != null || nextRenderingBitmap != null || nativePtr == 0 || loadingInBackground || destroyWhenDone || !isRunning && (!decodeSingleFrame || decodeSingleFrame && singleFrameDecoded)) { return false; } if (!newColorUpdates.isEmpty()) { @@ -739,6 +778,10 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { } public void setCurrentFrame(int frame, boolean async) { + setCurrentFrame(frame, true, false); + } + + public void setCurrentFrame(int frame, boolean async, boolean resetFrame) { if (frame < 0 || frame > metaData[0]) { return; } @@ -751,13 +794,13 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { doNotRemoveInvalidOnFrameReady = true; } } + if ((!async || resetFrame) && waitingForNextTask && nextRenderingBitmap != null) { + backgroundBitmap = nextRenderingBitmap; + nextRenderingBitmap = null; + loadFrameTask = null; + waitingForNextTask = false; + } if (!async) { - if (waitingForNextTask && nextRenderingBitmap != null) { - backgroundBitmap = nextRenderingBitmap; - nextRenderingBitmap = null; - loadFrameTask = null; - waitingForNextTask = false; - } if (loadFrameTask == null) { frameWaitSync = new CountDownLatch(1); } @@ -777,6 +820,11 @@ public class RLottieDrawable extends BitmapDrawable implements Animatable { invalidateSelf(); } + public void setProgressMs(long ms) { + int frameNum = (int) ((Math.max(0, ms) / timeBetweenFrames) % metaData[0]); + setCurrentFrame(frameNum, true, true); + } + public void setProgress(float progress) { setProgress(progress, true); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgress2.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgress2.java index bd390c6a6..d8e43a972 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgress2.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgress2.java @@ -162,6 +162,10 @@ public class RadialProgress2 { } } + public float getProgress() { + return drawMiniIcon ? miniMediaActionDrawable.getProgress() : mediaActionDrawable.getProgress(); + } + private void invalidateParent() { int offset = AndroidUtilities.dp(2); parent.invalidate((int) progressRect.left - offset, (int) progressRect.top - offset, (int) progressRect.right + offset * 2, (int) progressRect.bottom + offset * 2); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgressView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgressView.java index 0aaec16e2..add4ec027 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgressView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadialProgressView.java @@ -40,6 +40,13 @@ public class RadialProgressView extends View { private static final float risingTime = 500; private int size; + private float currentProgress; + private float progressAnimationStart; + private int progressTime; + private float animatedProgress; + + private boolean noProgress = true; + public RadialProgressView(Context context) { super(context); @@ -73,6 +80,20 @@ public class RadialProgressView extends View { } } + public void setNoProgress(boolean value) { + noProgress = value; + } + + public void setProgress(float value) { + currentProgress = value; + if (animatedProgress > value) { + animatedProgress = value; + } + progressAnimationStart = animatedProgress; + currentProgress = value; + progressTime = 0; + } + private void updateAnimation() { long newTime = System.currentTimeMillis(); long dt = newTime - lastUpdateTime; @@ -85,22 +106,36 @@ public class RadialProgressView extends View { int count = (int) (radOffset / 360); radOffset -= count * 360; - currentProgressTime += dt; - if (currentProgressTime >= risingTime) { - currentProgressTime = risingTime; - } - if (risingCircleLength) { - currentCircleLength = 4 + 266 * accelerateInterpolator.getInterpolation(currentProgressTime / risingTime); - } else { - currentCircleLength = 4 - 270 * (1.0f - decelerateInterpolator.getInterpolation(currentProgressTime / risingTime)); - } - if (currentProgressTime == risingTime) { - if (risingCircleLength) { - radOffset += 270; - currentCircleLength = -266; + if (noProgress) { + currentProgressTime += dt; + if (currentProgressTime >= risingTime) { + currentProgressTime = risingTime; } - risingCircleLength = !risingCircleLength; - currentProgressTime = 0; + if (risingCircleLength) { + currentCircleLength = 4 + 266 * accelerateInterpolator.getInterpolation(currentProgressTime / risingTime); + } else { + currentCircleLength = 4 - 270 * (1.0f - decelerateInterpolator.getInterpolation(currentProgressTime / risingTime)); + } + if (currentProgressTime == risingTime) { + if (risingCircleLength) { + radOffset += 270; + currentCircleLength = -266; + } + risingCircleLength = !risingCircleLength; + currentProgressTime = 0; + } + } else { + float progressDiff = currentProgress - progressAnimationStart; + if (progressDiff > 0) { + progressTime += dt; + if (progressTime >= 200.0f) { + animatedProgress = progressAnimationStart = currentProgress; + progressTime = 0; + } else { + animatedProgress = progressAnimationStart + progressDiff * AndroidUtilities.decelerateInterpolator.getInterpolation(progressTime / 200.0f); + } + } + currentCircleLength = Math.max(4, 360 * animatedProgress); } invalidate(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerAnimationScrollHelper.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerAnimationScrollHelper.java index 7c7804626..6b8a15e5e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerAnimationScrollHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerAnimationScrollHelper.java @@ -49,7 +49,7 @@ public class RecyclerAnimationScrollHelper { } public void scrollToPosition(int position, int offset, final boolean bottom, boolean smooth) { - if (recyclerView.scrollAnimationRunning || (recyclerView.getItemAnimator() != null && recyclerView.getItemAnimator().isRunning())) { + if (recyclerView.fastScrollAnimationRunning || (recyclerView.getItemAnimator() != null && recyclerView.getItemAnimator().isRunning())) { return; } if (!smooth || scrollDirection == SCROLL_DIRECTION_UNSET) { @@ -79,11 +79,8 @@ public class RecyclerAnimationScrollHelper { int childPosition = layoutManager.getPosition(child); positionToOldView.put(childPosition, child); if (adapter != null && adapter.hasStableIds()) { - if (child instanceof ChatMessageCell) { - oldStableIds.put((long) ((ChatMessageCell) child).getMessageObject().stableId, child); - } else { - oldStableIds.put(adapter.getItemId(childPosition), child); - } + long itemId = ((RecyclerView.LayoutParams) child.getLayoutParams()).mViewHolder.getItemId(); + oldStableIds.put(itemId, child); } if (child instanceof ChatMessageCell) { ((ChatMessageCell) child).setAnimationRunning(true, true); @@ -105,7 +102,7 @@ public class RecyclerAnimationScrollHelper { recyclerView.setVerticalScrollBarEnabled(false); if (animationCallback != null) animationCallback.onStartAnimation(); - recyclerView.scrollAnimationRunning = true; + recyclerView.fastScrollAnimationRunning = true; if (finalAnimatableAdapter != null) finalAnimatableAdapter.onAnimationStart(); recyclerView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @@ -118,6 +115,7 @@ public class RecyclerAnimationScrollHelper { int top = 0; int bottom = 0; int scrollDiff = 0; + boolean hasSameViews = false; for (int i = 0; i < n; i++) { View child = recyclerView.getChildAt(i); incomingViews.add(child); @@ -135,10 +133,14 @@ public class RecyclerAnimationScrollHelper { if (oldStableIds.containsKey(stableId)) { View view = oldStableIds.get(stableId); if (view != null) { - if (child instanceof ChatMessageCell) { - ((ChatMessageCell) child).setAnimationRunning(false, false); + hasSameViews = true; + if (view instanceof ChatMessageCell) { + ((ChatMessageCell) view).setAnimationRunning(false, false); } oldViews.remove(view); + if (animationCallback != null) { + animationCallback.recycleView(view); + } int dif = child.getTop() - view.getTop(); if (dif != 0) { scrollDiff = dif; @@ -151,7 +153,7 @@ public class RecyclerAnimationScrollHelper { oldStableIds.clear(); int oldH = 0; - int oldT = 0; + int oldT = Integer.MAX_VALUE; for (View view : oldViews) { int bot = view.getBottom(); @@ -168,6 +170,10 @@ public class RecyclerAnimationScrollHelper { } } + if (oldT == Integer.MAX_VALUE) { + oldT = 0; + } + final int scrollLength ; if (oldViews.isEmpty()) { scrollLength = Math.abs(scrollDiff); @@ -217,7 +223,7 @@ public class RecyclerAnimationScrollHelper { if (animator == null) { return; } - recyclerView.scrollAnimationRunning = false; + recyclerView.fastScrollAnimationRunning = false; for (View view : oldViews) { if (view instanceof ChatMessageCell) { @@ -226,6 +232,9 @@ public class RecyclerAnimationScrollHelper { view.setTranslationY(0); layoutManager.stopIgnoringView(view); recyclerView.removeView(view); + if (animationCallback != null) { + animationCallback.recycleView(view); + } } recyclerView.setVerticalScrollBarEnabled(true); @@ -238,10 +247,6 @@ public class RecyclerAnimationScrollHelper { if (recyclerView.mChildHelper.getHiddenChildCount() != 0) { throw new RuntimeException("hidden child count must be 0"); } - - if (recyclerView.getCachedChildCount() != 0) { - throw new RuntimeException("recycler cached child count must be 0"); - } } int n = recyclerView.getChildCount(); @@ -253,6 +258,13 @@ public class RecyclerAnimationScrollHelper { child.setTranslationY(0); } + for (View v : incomingViews) { + if (v instanceof ChatMessageCell) { + ((ChatMessageCell) v).setAnimationRunning(false, false); + } + v.setTranslationY(0); + } + if (finalAnimatableAdapter != null) { finalAnimatableAdapter.onAnimationEnd(); } @@ -269,13 +281,17 @@ public class RecyclerAnimationScrollHelper { recyclerView.removeOnLayoutChangeListener(this); - long duration = (long) (((scrollLength / (float) recyclerView.getMeasuredHeight()) + 1f) * 200L); - if (duration < 80) { - duration = 80; + long duration; + if (hasSameViews) { + duration = 600; + } else { + duration = (long) (((scrollLength / (float) recyclerView.getMeasuredHeight()) + 1f) * 200L); + if (duration < 300) { + duration = 300; + } + duration = Math.min(duration, 1300); } - duration = Math.min(duration, 1300); - animator.setDuration(duration); animator.setInterpolator(CubicBezierInterpolator.EASE_OUT_QUINT); animator.start(); @@ -290,7 +306,7 @@ public class RecyclerAnimationScrollHelper { private void clear() { recyclerView.setVerticalScrollBarEnabled(true); - recyclerView.scrollAnimationRunning = false; + recyclerView.fastScrollAnimationRunning = false; RecyclerView.Adapter adapter = recyclerView.getAdapter(); if (adapter instanceof AnimatableAdapter) ((AnimatableAdapter) adapter).onAnimationEnd(); @@ -332,6 +348,10 @@ public class RecyclerAnimationScrollHelper { public void onEndAnimation() { } + + public void recycleView(View view) { + + } } public static abstract class AnimatableAdapter extends RecyclerListView.SelectionAdapter { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java index 096f01f45..3320f848c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RecyclerListView.java @@ -8,6 +8,8 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; @@ -118,7 +120,8 @@ public class RecyclerListView extends RecyclerView { private static boolean gotAttributes; private boolean hiddenByEmptyView; - public boolean scrollAnimationRunning; + public boolean fastScrollAnimationRunning; + private boolean animateEmptyView; public interface OnItemClickListener { void onItemClick(View view, int position); @@ -829,7 +832,7 @@ public class RecyclerListView extends RecyclerView { private AdapterDataObserver observer = new AdapterDataObserver() { @Override public void onChanged() { - checkIfEmpty(); + checkIfEmpty(true); currentFirst = -1; if (removeHighlighSelectionRunnable == null) { selectorRect.setEmpty(); @@ -839,12 +842,12 @@ public class RecyclerListView extends RecyclerView { @Override public void onItemRangeInserted(int positionStart, int itemCount) { - checkIfEmpty(); + checkIfEmpty(true); } @Override public void onItemRangeRemoved(int positionStart, int itemCount) { - checkIfEmpty(); + checkIfEmpty(true); } }; @@ -1220,13 +1223,18 @@ public class RecyclerListView extends RecyclerView { if (emptyView == view) { return; } + if (emptyView != null) { + emptyView.animate().setListener(null).cancel(); + } emptyView = view; if (isHidden) { if (emptyView != null) { + emptyViewAnimateToVisibility = GONE; emptyView.setVisibility(GONE); } } else { - checkIfEmpty(); + emptyViewAnimateToVisibility = -1; + checkIfEmpty(false); } } @@ -1327,7 +1335,9 @@ public class RecyclerListView extends RecyclerView { return super.dispatchTouchEvent(ev); } - private void checkIfEmpty() { + int emptyViewAnimateToVisibility; + + private void checkIfEmpty(boolean animated) { if (isHidden) { return; } @@ -1340,8 +1350,36 @@ public class RecyclerListView extends RecyclerView { } boolean emptyViewVisible = getAdapter().getItemCount() == 0; int newVisibility = emptyViewVisible ? VISIBLE : GONE; - if (emptyView.getVisibility() != newVisibility) { + if (!animateEmptyView || !isAttachedToWindow()) { + animated = false; + } + if (animated) { + if (emptyViewAnimateToVisibility != newVisibility) { + emptyViewAnimateToVisibility = newVisibility; + if (newVisibility == VISIBLE) { + emptyView.animate().setListener(null).cancel(); + if (emptyView.getVisibility() == GONE) { + emptyView.setVisibility(VISIBLE); + emptyView.setAlpha(0); + } + emptyView.animate().alpha(1f).setDuration(150).start(); + } else { + if (emptyView.getVisibility() != GONE) { + emptyView.animate().alpha(0).setDuration(150).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (emptyView != null) { + emptyView.setVisibility(GONE); + } + } + }).start(); + } + } + } + } else { + emptyViewAnimateToVisibility = newVisibility; emptyView.setVisibility(newVisibility); + emptyView.setAlpha(1f); } if (hideIfEmpty) { newVisibility = emptyViewVisible ? INVISIBLE : VISIBLE; @@ -1370,7 +1408,7 @@ public class RecyclerListView extends RecyclerView { return; } isHidden = false; - checkIfEmpty(); + checkIfEmpty(false); } @Override @@ -1584,7 +1622,7 @@ public class RecyclerListView extends RecyclerView { if (adapter != null) { adapter.registerAdapterDataObserver(observer); } - checkIfEmpty(); + checkIfEmpty(false); } @Override @@ -1776,18 +1814,22 @@ public class RecyclerListView extends RecyclerView { return pinnedHeader; } - public boolean isScrollAnimationRunning() { - return scrollAnimationRunning; + public boolean isFastScrollAnimationRunning() { + return fastScrollAnimationRunning; } @Override public void requestLayout() { - if (scrollAnimationRunning) { + if (fastScrollAnimationRunning) { return; } super.requestLayout(); } + public void setAnimateEmptyView(boolean animate) { + animateEmptyView = animate; + } + public static class FoucsableOnTouchListener implements OnTouchListener { private float x; private float y; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundVideoPlayingDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundVideoPlayingDrawable.java index 093e02471..0d3f89e80 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundVideoPlayingDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RoundVideoPlayingDrawable.java @@ -11,6 +11,7 @@ package org.telegram.ui.Components; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; +import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.view.View; @@ -90,7 +91,7 @@ public class RoundVideoPlayingDrawable extends Drawable { @Override public void draw(Canvas canvas) { - paint.setColor(Theme.getColor(Theme.key_chat_mediaTimeText)); + paint.setColor(Theme.getColor(Theme.key_chat_serviceText)); int x = getBounds().left; int y = getBounds().top; for (int a = 0; a < 3; a++) { @@ -115,7 +116,7 @@ public class RoundVideoPlayingDrawable extends Drawable { @Override public int getOpacity() { - return 0; + return PixelFormat.TRANSPARENT; } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBar.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBar.java index e4a0819f7..ed1478bb2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBar.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBar.java @@ -70,7 +70,7 @@ public class SeekBar { if (thumbX < 0) { thumbX = 0; } else if (thumbX > width - thumbWidth) { - thumbX = thumbWidth - width; + thumbX = width - thumbWidth; } } pressed = true; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarAccessibilityDelegate.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarAccessibilityDelegate.java new file mode 100644 index 000000000..230550b82 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarAccessibilityDelegate.java @@ -0,0 +1,108 @@ +package org.telegram.ui.Components; + +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.SeekBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; + +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; + +public abstract class SeekBarAccessibilityDelegate extends View.AccessibilityDelegate { + + private static final CharSequence SEEK_BAR_CLASS_NAME = SeekBar.class.getName(); + + private final Map accessibilityEventRunnables = new HashMap<>(4); + private final View.OnAttachStateChangeListener onAttachStateChangeListener = new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + v.removeCallbacks(accessibilityEventRunnables.remove(v)); + v.removeOnAttachStateChangeListener(this); + } + }; + + @Override + public boolean performAccessibilityAction(@NonNull View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + return performAccessibilityActionInternal(host, action, args); + } + + public boolean performAccessibilityActionInternal(@Nullable View host, int action, Bundle args) { + if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD || action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + doScroll(host, action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + if (host != null) { + postAccessibilityEventRunnable(host); + } + return true; + } + return false; + } + + public final boolean performAccessibilityActionInternal(int action, Bundle args) { + return performAccessibilityActionInternal(null, action, args); + } + + private void postAccessibilityEventRunnable(@NonNull View host) { + if (!ViewCompat.isAttachedToWindow(host)) { + return; + } + Runnable runnable = accessibilityEventRunnables.get(host); + if (runnable == null) { + accessibilityEventRunnables.put(host, runnable = () -> sendAccessibilityEvent(host, AccessibilityEvent.TYPE_VIEW_SELECTED)); + host.addOnAttachStateChangeListener(onAttachStateChangeListener); + } else { + host.removeCallbacks(runnable); + } + host.postDelayed(runnable, 400); + } + + @Override + public void onInitializeAccessibilityNodeInfo(@NonNull View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + onInitializeAccessibilityNodeInfoInternal(host, info); + } + + public void onInitializeAccessibilityNodeInfoInternal(@Nullable View host, AccessibilityNodeInfo info) { + info.setClassName(SEEK_BAR_CLASS_NAME); + final CharSequence contentDescription = getContentDescription(host); + if (!TextUtils.isEmpty(contentDescription)) { + info.setText(contentDescription); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (canScrollBackward(host)) { + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); + } + if (canScrollForward(host)) { + info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); + } + } + } + + public final void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { + onInitializeAccessibilityNodeInfoInternal(null, info); + } + + protected CharSequence getContentDescription(@Nullable View host) { + return null; + } + + protected abstract void doScroll(@Nullable View host, boolean backward); + + protected abstract boolean canScrollBackward(@Nullable View host); + + protected abstract boolean canScrollForward(@Nullable View host); +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarView.java index 0876b476b..df83770ef 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarView.java @@ -17,6 +17,7 @@ import android.os.Build; import android.os.SystemClock; import android.util.StateSet; import android.view.MotionEvent; +import android.view.View; import android.view.ViewConfiguration; import android.widget.FrameLayout; @@ -25,6 +26,8 @@ import org.telegram.ui.ActionBar.Theme; public class SeekBarView extends FrameLayout { + private final SeekBarAccessibilityDelegate seekBarAccessibilityDelegate; + private Paint innerPaint1; private Paint outerPaint1; private int thumbSize; @@ -44,13 +47,22 @@ public class SeekBarView extends FrameLayout { public interface SeekBarViewDelegate { void onSeekBarDrag(boolean stop, float progress); void onSeekBarPressed(boolean pressed); + default CharSequence getContentDescription() { + return null; + } + default int getStepsCount() { + return 0; + } } public SeekBarView(Context context) { + this(context, false); + } + + public SeekBarView(Context context, boolean inPercents) { super(context); setWillNotDraw(false); innerPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG); - innerPaint1.setColor(Theme.getColor(Theme.key_player_progressBackground)); outerPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG); outerPaint1.setColor(Theme.getColor(Theme.key_player_progress)); @@ -65,6 +77,39 @@ public class SeekBarView extends FrameLayout { hoverDrawable.setCallback(this); hoverDrawable.setVisible(true, false); } + + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + setAccessibilityDelegate(seekBarAccessibilityDelegate = new FloatSeekBarAccessibilityDelegate(inPercents) { + @Override + public float getProgress() { + return SeekBarView.this.getProgress(); + } + + @Override + public void setProgress(float progress) { + pressed = true; + SeekBarView.this.setProgress(progress); + if (delegate != null) { + delegate.onSeekBarDrag(true, progress); + } + pressed = false; + } + + @Override + protected float getDelta() { + final int stepsCount = delegate.getStepsCount(); + if (stepsCount > 0) { + return 1f / stepsCount; + } else { + return super.getDelta(); + } + } + + @Override + public CharSequence getContentDescription(View host) { + return delegate != null ? delegate.getContentDescription() : null; + } + }); } public void setColors(int inner, int outer) { @@ -220,6 +265,7 @@ public class SeekBarView extends FrameLayout { public void setBufferedProgress(float progress) { bufferedProgress = progress; + invalidate(); } @Override @@ -243,8 +289,10 @@ public class SeekBarView extends FrameLayout { @Override protected void onDraw(Canvas canvas) { int y = (getMeasuredHeight() - thumbSize) / 2; + innerPaint1.setColor(Theme.getColor(Theme.key_player_progressBackground)); canvas.drawRect(selectorWidth / 2, getMeasuredHeight() / 2 - AndroidUtilities.dp(1), getMeasuredWidth() - selectorWidth / 2, getMeasuredHeight() / 2 + AndroidUtilities.dp(1), innerPaint1); if (bufferedProgress > 0) { + innerPaint1.setColor(Theme.getColor(Theme.key_player_progressCachedBackground)); canvas.drawRect(selectorWidth / 2, getMeasuredHeight() / 2 - AndroidUtilities.dp(1), selectorWidth / 2 + bufferedProgress * (getMeasuredWidth() - selectorWidth), getMeasuredHeight() / 2 + AndroidUtilities.dp(1), innerPaint1); } canvas.drawRect(selectorWidth / 2, getMeasuredHeight() / 2 - AndroidUtilities.dp(1), selectorWidth / 2 + thumbX, getMeasuredHeight() / 2 + AndroidUtilities.dp(1), outerPaint1); @@ -276,4 +324,8 @@ public class SeekBarView extends FrameLayout { } canvas.drawCircle(thumbX + selectorWidth / 2, y + thumbSize / 2, currentRadius, outerPaint1); } + + public SeekBarAccessibilityDelegate getSeekBarAccessibilityDelegate() { + return seekBarAccessibilityDelegate; + } } 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 0269c3fde..9b225596f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java @@ -124,6 +124,10 @@ public class SeekBarWaveform { return false; } + public float getProgress() { + return thumbX / (float) width; + } + public void setProgress(float progress) { thumbX = (int) Math.ceil(width * progress); if (thumbX < 0) { 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 c64faeef8..42a6372dc 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java @@ -35,9 +35,11 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.DecelerateInterpolator; import android.view.animation.OvershootInterpolator; import android.view.inputmethod.EditorInfo; +import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; @@ -73,6 +75,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Locale; +import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -160,6 +163,9 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi public boolean dispatchTouchEvent(MotionEvent event) { MotionEvent e = MotionEvent.obtain(event); e.setLocation(e.getRawX(), e.getRawY() - containerView.getTranslationY()); + if (e.getAction() == MotionEvent.ACTION_UP) { + e.setAction(MotionEvent.ACTION_CANCEL); + } gridView.dispatchTouchEvent(e); e.recycle(); return super.dispatchTouchEvent(event); @@ -235,11 +241,6 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi public void hideKeyboard() { AndroidUtilities.hideKeyboard(searchEditText); } - - @Override - public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { - super.requestDisallowInterceptTouchEvent(disallowIntercept); - } } public static ShareAlert createShareAlert(final Context context, MessageObject messageObject, final String text, boolean channel, final String copyLink, boolean fullScreen) { @@ -694,12 +695,22 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi editText.setSingleLine(true); frameLayout2.addView(commentTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 0, 84, 0)); - writeButtonContainer = new FrameLayout(context); + writeButtonContainer = new FrameLayout(context) { + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setText(LocaleController.formatPluralString("AccDescrShareInChats", selectedDialogs.size())); + info.setClassName(Button.class.getName()); + info.setLongClickable(true); + info.setClickable(true); + } + }; + writeButtonContainer.setFocusable(true); + writeButtonContainer.setFocusableInTouchMode(true); writeButtonContainer.setVisibility(View.INVISIBLE); writeButtonContainer.setScaleX(0.2f); writeButtonContainer.setScaleY(0.2f); writeButtonContainer.setAlpha(0.0f); - writeButtonContainer.setContentDescription(LocaleController.getString("Send", R.string.Send)); containerView.addView(writeButtonContainer, LayoutHelper.createFrame(60, 60, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 6, 10)); ImageView writeButton = new ImageView(context); @@ -713,6 +724,7 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi } writeButton.setBackgroundDrawable(drawable); writeButton.setImageResource(R.drawable.attach_send); + writeButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); writeButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingIcon), PorterDuff.Mode.SRC_IN)); writeButton.setScaleType(ImageView.ScaleType.CENTER); if (Build.VERSION.SDK_INT >= 21) { @@ -935,6 +947,9 @@ public class ShareAlert extends BottomSheet implements NotificationCenter.Notifi frameLayout2.setVisibility(View.VISIBLE); writeButtonContainer.setVisibility(View.VISIBLE); } + if (pickerBottomLayout != null) { + ViewCompat.setImportantForAccessibility(pickerBottomLayout, show ? ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS : ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } animatorSet = new AnimatorSet(); ArrayList animators = new ArrayList<>(); animators.add(ObjectAnimator.ofFloat(frameLayout2, View.ALPHA, show ? 1.0f : 0.0f)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SharedMediaLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SharedMediaLayout.java index 33ecbf6f0..4363f7c59 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SharedMediaLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SharedMediaLayout.java @@ -148,6 +148,10 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter private Runnable hideFloatingDateRunnable = () -> hideFloatingDateView(true); private ArrayList actionModeViews = new ArrayList<>(); + private float additionalFloatingTranslation; + + private FragmentContextView fragmentContextView; + private int maximumVelocity; private Paint backgroundPaint = new Paint(); @@ -521,11 +525,17 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter object.thumb = object.imageReceiver.getBitmapSafe(); object.parentView.getLocationInWindow(coords); object.clipTopAddition = 0; + if (fragmentContextView != null && fragmentContextView.getVisibility() == View.VISIBLE) { + object.clipTopAddition += AndroidUtilities.dp(36); + } if (PhotoViewer.isShowingImage(messageObject)) { final View pinnedHeader = listView.getPinnedHeader(); if (pinnedHeader != null) { int top = 0; + if (fragmentContextView != null && fragmentContextView.getVisibility() == View.VISIBLE) { + top += fragmentContextView.getHeight() - AndroidUtilities.dp(2.5f); + } if (view instanceof SharedDocumentCell) { top += AndroidUtilities.dp(8f); } @@ -708,7 +718,7 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter MediaController.getInstance().setVoiceMessagesPlaylist(result ? sharedMediaData[MediaDataController.MEDIA_MUSIC].messages : null, false); return result; } else if (messageObject.isMusic()) { - return MediaController.getInstance().setPlaylist(sharedMediaData[MediaDataController.MEDIA_MUSIC].messages, messageObject); + return MediaController.getInstance().setPlaylist(sharedMediaData[MediaDataController.MEDIA_MUSIC].messages, messageObject, mergeDialogId); } return false; } @@ -883,6 +893,7 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter closeButton.setImageDrawable(backDrawable = new BackDrawable(true)); backDrawable.setColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2)); closeButton.setBackground(Theme.createSelectorDrawable(Theme.getColor(Theme.key_actionBarActionModeDefaultSelector), 1)); + closeButton.setContentDescription(LocaleController.getString("Close", R.string.Close)); actionModeLayout.addView(closeButton, new LinearLayout.LayoutParams(AndroidUtilities.dp(54), ViewGroup.LayoutParams.MATCH_PARENT)); actionModeViews.add(closeButton); closeButton.setOnClickListener(v -> closeActionMode()); @@ -1102,15 +1113,9 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter int user_id; TLObject object = groupUsersSearchAdapter.getItem(position); if (object instanceof TLRPC.ChannelParticipant) { - if (object instanceof TLRPC.TL_channelParticipantCreator) { - return; - } TLRPC.ChannelParticipant channelParticipant = (TLRPC.ChannelParticipant) object; user_id = channelParticipant.user_id; } else if (object instanceof TLRPC.ChatParticipant) { - if (object instanceof TLRPC.TL_chatParticipantCreator) { - return; - } TLRPC.ChatParticipant chatParticipant = (TLRPC.ChatParticipant) object; user_id = chatParticipant.user_id; } else { @@ -1243,6 +1248,23 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter floatingDateView.setTranslationY(-AndroidUtilities.dp(48)); addView(floatingDateView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, 48 + 4, 0, 0)); + addView(fragmentContextView = new FragmentContextView(context, parent, this, false), LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 39, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); + fragmentContextView.setDelegate((start, show) -> { + if (show && !start) { + for (int a = 0; a < mediaPages.length; a++) { + int translation = (int) mediaPages[a].getTranslationY(); + mediaPages[a].setTranslationY(0); + mediaPages[a].setPadding(0, translation, 0, 0); + } + } else if (!show && start) { + for (int a = 0; a < mediaPages.length; a++) { + int translation = mediaPages[a].getPaddingTop(); + mediaPages[a].setTranslationY(translation); + mediaPages[a].setPadding(0, 0, 0, 0); + } + } + }); + addView(scrollSlidingTextTabStrip, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.TOP)); addView(actionModeLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.TOP)); @@ -1289,7 +1311,7 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter floatingDateAnimation.setDuration(180); floatingDateAnimation.playTogether( ObjectAnimator.ofFloat(floatingDateView, View.ALPHA, 1.0f), - ObjectAnimator.ofFloat(floatingDateView, View.TRANSLATION_Y, 0)); + ObjectAnimator.ofFloat(floatingDateView, View.TRANSLATION_Y, additionalFloatingTranslation)); floatingDateAnimation.setInterpolator(CubicBezierInterpolator.EASE_OUT); floatingDateAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -1315,7 +1337,7 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter floatingDateAnimation.setDuration(180); floatingDateAnimation.playTogether( ObjectAnimator.ofFloat(floatingDateView, View.ALPHA, 0.0f), - ObjectAnimator.ofFloat(floatingDateView, View.TRANSLATION_Y, -AndroidUtilities.dp(48))); + ObjectAnimator.ofFloat(floatingDateView, View.TRANSLATION_Y, -AndroidUtilities.dp(48) + additionalFloatingTranslation)); floatingDateAnimation.setInterpolator(CubicBezierInterpolator.EASE_OUT); floatingDateAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -1661,6 +1683,16 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter super.forceHasOverlappingRendering(hasOverlappingRendering); } + @Override + public void setPadding(int left, int top, int right, int bottom) { + for (int a = 0; a < mediaPages.length; a++) { + mediaPages[a].setTranslationY(top); + } + fragmentContextView.setTranslationY(AndroidUtilities.dp(48) + top); + additionalFloatingTranslation = top; + floatingDateView.setTranslationY((floatingDateView.getTag() == null ? -AndroidUtilities.dp(48) : 0) + additionalFloatingTranslation); + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); @@ -2879,7 +2911,7 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter } private void openWebView(TLRPC.WebPage webPage) { - EmbedBottomSheet.show(profileActivity.getParentActivity(), webPage.site_name, webPage.description, webPage.url, webPage.embed_url, webPage.embed_width, webPage.embed_height); + EmbedBottomSheet.show(profileActivity.getParentActivity(), webPage.site_name, webPage.description, webPage.url, webPage.embed_url, webPage.embed_width, webPage.embed_height, false); } private void recycleAdapter(RecyclerView.Adapter adapter) { @@ -3159,7 +3191,7 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter MediaController.getInstance().setVoiceMessagesPlaylist(result ? sharedMediaData[currentType].messages : null, false); return result; } else if (messageObject.isMusic()) { - return MediaController.getInstance().setPlaylist(sharedMediaData[currentType].messages, messageObject); + return MediaController.getInstance().setPlaylist(sharedMediaData[currentType].messages, messageObject, mergeDialogId); } return false; } @@ -3607,7 +3639,7 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter } return result; } else if (messageObject.isMusic()) { - return MediaController.getInstance().setPlaylist(searchResult, messageObject); + return MediaController.getInstance().setPlaylist(searchResult, messageObject, mergeDialogId); } return false; } @@ -3668,7 +3700,7 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { - return false; + return true; } @Override @@ -3689,7 +3721,6 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { ContextLinkCell cell = new ContextLinkCell(mContext, true); - cell.setContentDescription(LocaleController.getString("AttachGif", R.string.AttachGif)); cell.setCanPreviewGif(true); return new RecyclerListView.Holder(cell); } @@ -4172,6 +4203,15 @@ public class SharedMediaLayout extends FrameLayout implements NotificationCenter arrayList.add(new ThemeDescription(scrollSlidingTextTabStrip.getTabsContainer(), ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, new Class[]{TextView.class}, null, null, null, Theme.key_profile_tabText)); arrayList.add(new ThemeDescription(scrollSlidingTextTabStrip.getTabsContainer(), ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_DRAWABLESELECTEDSTATE, new Class[]{TextView.class}, null, null, null, Theme.key_profile_tabSelector)); + arrayList.add(new ThemeDescription(fragmentContextView, ThemeDescription.FLAG_BACKGROUND | ThemeDescription.FLAG_CHECKTAG, new Class[]{FragmentContextView.class}, new String[]{"frameLayout"}, null, null, null, Theme.key_inappPlayerBackground)); + arrayList.add(new ThemeDescription(fragmentContextView, ThemeDescription.FLAG_IMAGECOLOR, new Class[]{FragmentContextView.class}, new String[]{"playButton"}, null, null, null, Theme.key_inappPlayerPlayPause)); + arrayList.add(new ThemeDescription(fragmentContextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, new Class[]{FragmentContextView.class}, new String[]{"titleTextView"}, null, null, null, Theme.key_inappPlayerTitle)); + arrayList.add(new ThemeDescription(fragmentContextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_FASTSCROLL, new Class[]{FragmentContextView.class}, new String[]{"titleTextView"}, null, null, null, Theme.key_inappPlayerPerformer)); + arrayList.add(new ThemeDescription(fragmentContextView, ThemeDescription.FLAG_IMAGECOLOR, new Class[]{FragmentContextView.class}, new String[]{"closeButton"}, null, null, null, Theme.key_inappPlayerClose)); + + arrayList.add(new ThemeDescription(fragmentContextView, ThemeDescription.FLAG_BACKGROUND | ThemeDescription.FLAG_CHECKTAG, new Class[]{FragmentContextView.class}, new String[]{"frameLayout"}, null, null, null, Theme.key_returnToCallBackground)); + arrayList.add(new ThemeDescription(fragmentContextView, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, new Class[]{FragmentContextView.class}, new String[]{"titleTextView"}, null, null, null, Theme.key_returnToCallText)); + for (int a = 0; a < mediaPages.length; a++) { final int num = a; ThemeDescription.ThemeDescriptionDelegate cellDelegate = () -> { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SlideChooseView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SlideChooseView.java index 90eb8d945..e595d7961 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SlideChooseView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SlideChooseView.java @@ -3,15 +3,19 @@ package org.telegram.ui.Components; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; +import android.os.Bundle; import android.text.TextPaint; import android.view.MotionEvent; import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; import org.telegram.messenger.AndroidUtilities; import org.telegram.ui.ActionBar.Theme; public class SlideChooseView extends View { + private final SeekBarAccessibilityDelegate accessibilityDelegate; + private Paint paint; private TextPaint textPaint; @@ -39,11 +43,34 @@ public class SlideChooseView extends View { paint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); textPaint.setTextSize(AndroidUtilities.dp(13)); + + accessibilityDelegate = new IntSeekBarAccessibilityDelegate() { + @Override + protected int getProgress() { + return selectedIndex; + } + + @Override + protected void setProgress(int progress) { + setOption(progress); + } + + @Override + protected int getMaxValue() { + return optionsStr.length - 1; + } + + @Override + protected CharSequence getContentDescription(View host) { + return selectedIndex < optionsStr.length ? optionsStr[selectedIndex] : null; + } + }; } public void setCallback(Callback callback) { this.callback = callback; } + public void setOptions(int selected, String... options) { this.callback = callback; this.optionsStr = options; @@ -165,6 +192,17 @@ public class SlideChooseView extends View { } } + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + accessibilityDelegate.onInitializeAccessibilityNodeInfoInternal(this, info); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + return super.performAccessibilityAction(action, arguments) || accessibilityDelegate.performAccessibilityActionInternal(this, action, arguments); + } + public interface Callback { void onOptionSelected(int index); } 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 970bdbe59..c44fad1c9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java @@ -745,7 +745,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not if (installDelegate != null) { installDelegate.onStickerSetInstalled(); } - if (MediaDataController.getInstance(currentAccount).cancelRemovingStickerSet(inputStickerSet.id)) { + if (inputStickerSet == null || MediaDataController.getInstance(currentAccount).cancelRemovingStickerSet(inputStickerSet.id)) { return; } TLRPC.TL_messages_installStickerSet req = new TLRPC.TL_messages_installStickerSet(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/TimerDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/TimerDrawable.java index 5cfa2341d..a1dcc42e4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/TimerDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/TimerDrawable.java @@ -12,6 +12,7 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; +import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.text.Layout; import android.text.StaticLayout; @@ -19,6 +20,8 @@ import android.text.TextPaint; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.FileLog; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; import org.telegram.ui.ActionBar.Theme; public class TimerDrawable extends Drawable { @@ -46,31 +49,36 @@ public class TimerDrawable extends Drawable { if (time >= 1 && time < 60) { timeString = "" + value; if (timeString.length() < 2) { - timeString += "s"; + timeString += LocaleController.getString("SecretChatTimerSeconds", R.string.SecretChatTimerSeconds); } } else if (time >= 60 && time < 60 * 60) { timeString = "" + value / 60; if (timeString.length() < 2) { - timeString += "m"; + timeString += LocaleController.getString("SecretChatTimerMinutes", R.string.SecretChatTimerMinutes); } } else if (time >= 60 * 60 && time < 60 * 60 * 24) { timeString = "" + value / 60 / 60; if (timeString.length() < 2) { - timeString += "h"; + timeString += LocaleController.getString("SecretChatTimerHours", R.string.SecretChatTimerHours); } } else if (time >= 60 * 60 * 24 && time < 60 * 60 * 24 * 7) { timeString = "" + value / 60 / 60 / 24; if (timeString.length() < 2) { - timeString += "d"; + timeString += LocaleController.getString("SecretChatTimerDays", R.string.SecretChatTimerDays); } } else { timeString = "" + value / 60 / 60 / 24 / 7; if (timeString.length() < 2) { - timeString += "w"; + timeString += LocaleController.getString("SecretChatTimerWeeks", R.string.SecretChatTimerWeeks); } else if (timeString.length() > 2) { timeString = "c"; } } + /* + d + s + m + */ timeWidth = timePaint.measureText(timeString); try { @@ -130,7 +138,7 @@ public class TimerDrawable extends Drawable { @Override public int getOpacity() { - return 0; + return PixelFormat.TRANSPARENT; } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Tooltip.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Tooltip.java index 6d3b5ec80..e4a50601b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Tooltip.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Tooltip.java @@ -64,7 +64,11 @@ public class Tooltip extends TextView { view = (View) view.getParent(); } int x = left + anchor.getWidth() / 2 - getMeasuredWidth() / 2; - if (x < 0) x = 0; + if (x < 0) { + x = 0; + } else if (x + getMeasuredWidth() > containerView.getMeasuredWidth()) { + x = containerView.getMeasuredWidth() - getMeasuredWidth() - AndroidUtilities.dp(16); + } setTranslationX(x); int y = top - getMeasuredHeight(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/UndoView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/UndoView.java index 704f4a755..19ac02d6c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/UndoView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/UndoView.java @@ -104,6 +104,8 @@ public class UndoView extends FrameLayout { public final static int ACTION_CACHE_WAS_CLEARED = 19; public final static int ACTION_ADDED_TO_FOLDER = 20; public final static int ACTION_REMOVED_FROM_FOLDER = 21; + public final static int ACTION_PROFILE_PHOTO_CHANGED = 22; + public final static int ACTION_CHAT_UNARCHIVED = 23; private CharSequence infoText; @@ -216,11 +218,20 @@ public class UndoView extends FrameLayout { setVisibility(INVISIBLE); } + public void setColors(int background, int text) { + Theme.setDrawableColor(getBackground(), background); + infoTextView.setTextColor(text); + subinfoTextView.setTextColor(text); + leftImageView.setLayerColor("info1.**", background | 0xff000000); + leftImageView.setLayerColor("info2.**", background | 0xff000000); + } + private boolean isTooltipAction() { return currentAction == ACTION_ARCHIVE_HIDDEN || currentAction == ACTION_ARCHIVE_HINT || currentAction == ACTION_ARCHIVE_FEW_HINT || currentAction == ACTION_ARCHIVE_PINNED || currentAction == ACTION_CONTACT_ADDED || currentAction == ACTION_OWNER_TRANSFERED_CHANNEL || currentAction == ACTION_OWNER_TRANSFERED_GROUP || currentAction == ACTION_QUIZ_CORRECT || currentAction == ACTION_QUIZ_INCORRECT || currentAction == ACTION_CACHE_WAS_CLEARED || - currentAction == ACTION_ADDED_TO_FOLDER || currentAction == ACTION_REMOVED_FROM_FOLDER; + currentAction == ACTION_ADDED_TO_FOLDER || currentAction == ACTION_REMOVED_FROM_FOLDER || currentAction == ACTION_PROFILE_PHOTO_CHANGED || + currentAction == ACTION_CHAT_UNARCHIVED; } private boolean hasSubInfo() { @@ -366,6 +377,35 @@ public class UndoView extends FrameLayout { infoText = LocaleController.formatString("NowInContacts", R.string.NowInContacts, UserObject.getFirstName(user)); subInfoText = null; icon = R.raw.contact_check; + } else if (action == ACTION_PROFILE_PHOTO_CHANGED) { + if (did > 0) { + if (infoObject == null) { + infoText = LocaleController.getString("MainProfilePhotoSetHint", R.string.MainProfilePhotoSetHint); + } else { + infoText = LocaleController.getString("MainProfileVideoSetHint", R.string.MainProfileVideoSetHint); + } + } else { + TLRPC.Chat chat = MessagesController.getInstance(UserConfig.selectedAccount).getChat((int) -did); + if (ChatObject.isChannel(chat) && !chat.megagroup) { + if (infoObject == null) { + infoText = LocaleController.getString("MainChannelProfilePhotoSetHint", R.string.MainChannelProfilePhotoSetHint); + } else { + infoText = LocaleController.getString("MainChannelProfileVideoSetHint", R.string.MainChannelProfileVideoSetHint); + } + } else { + if (infoObject == null) { + infoText = LocaleController.getString("MainGroupProfilePhotoSetHint", R.string.MainGroupProfilePhotoSetHint); + } else { + infoText = LocaleController.getString("MainGroupProfileVideoSetHint", R.string.MainGroupProfileVideoSetHint); + } + } + } + subInfoText = null; + icon = R.raw.contact_check; + } else if (action == ACTION_CHAT_UNARCHIVED) { + infoText = LocaleController.getString("ChatWasMovedToMainList", R.string.ChatWasMovedToMainList); + subInfoText = null; + icon = R.raw.contact_check; } else if (action == ACTION_ARCHIVE_HIDDEN) { infoText = LocaleController.getString("ArchiveHidden", R.string.ArchiveHidden); subInfoText = LocaleController.getString("ArchiveHiddenInfo", R.string.ArchiveHiddenInfo); @@ -544,7 +584,12 @@ public class UndoView extends FrameLayout { if ("\uD83C\uDFAF".equals(emoji)) { infoTextView.setText(AndroidUtilities.replaceTags(LocaleController.getString("DartInfo", R.string.DartInfo))); } else { - infoTextView.setText(Emoji.replaceEmoji(LocaleController.formatString("DiceEmojiInfo", R.string.DiceEmojiInfo, emoji), infoTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(14), false)); + String info = LocaleController.getServerString("DiceEmojiInfo_" + emoji); + if (!TextUtils.isEmpty(info)) { + infoTextView.setText(Emoji.replaceEmoji(info, infoTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(14), false)); + } else { + infoTextView.setText(Emoji.replaceEmoji(LocaleController.formatString("DiceEmojiInfo", R.string.DiceEmojiInfo, emoji), infoTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(14), false)); + } } leftImageView.setImageDrawable(Emoji.getEmojiDrawable(emoji)); leftImageView.setScaleType(ImageView.ScaleType.FIT_XY); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VerticalPositionAutoAnimator.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VerticalPositionAutoAnimator.java new file mode 100644 index 000000000..4b752fe16 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VerticalPositionAutoAnimator.java @@ -0,0 +1,66 @@ +package org.telegram.ui.Components; + +import android.view.View; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +import org.telegram.messenger.AndroidUtilities; + +public final class VerticalPositionAutoAnimator { + + public static VerticalPositionAutoAnimator attach(View floatingButtonView) { + return attach(floatingButtonView, 350); + } + + public static VerticalPositionAutoAnimator attach(View floatingButtonView, float springStiffness) { + final AnimatorLayoutChangeListener listener = new AnimatorLayoutChangeListener(floatingButtonView, springStiffness); + floatingButtonView.addOnLayoutChangeListener(listener); + return new VerticalPositionAutoAnimator(listener); + } + + private final AnimatorLayoutChangeListener animatorLayoutChangeListener; + + private VerticalPositionAutoAnimator(AnimatorLayoutChangeListener animatorLayoutChangeListener) { + this.animatorLayoutChangeListener = animatorLayoutChangeListener; + } + + public void ignoreNextLayout() { + animatorLayoutChangeListener.ignoreNextLayout = true; + } + + private static class AnimatorLayoutChangeListener implements View.OnLayoutChangeListener { + + private final SpringAnimation floatingButtonAnimator; + + private Boolean orientation; + private boolean ignoreNextLayout; + + public AnimatorLayoutChangeListener(View view, float springStiffness) { + floatingButtonAnimator = new SpringAnimation(view, DynamicAnimation.TRANSLATION_Y, 0); + floatingButtonAnimator.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY); + floatingButtonAnimator.getSpring().setStiffness(springStiffness); + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + checkOrientation(); + if (oldTop == 0 || oldTop == top || ignoreNextLayout) { + ignoreNextLayout = false; + return; + } + floatingButtonAnimator.cancel(); + v.setTranslationY(oldTop - top); + floatingButtonAnimator.start(); + } + + private void checkOrientation() { + final boolean orientation = AndroidUtilities.displaySize.x > AndroidUtilities.displaySize.y; + if (this.orientation == null || this.orientation != orientation) { + this.orientation = orientation; + ignoreNextLayout = true; + } + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java index 83ba5dcc0..6939b6397 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.TeeAudioProcessor; @@ -67,7 +68,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; @SuppressLint("NewApi") -public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.VideoListener, NotificationCenter.NotificationCenterDelegate { +public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.VideoListener, AnalyticsListener, NotificationCenter.NotificationCenterDelegate { public interface VideoPlayerDelegate { void onStateChanged(boolean playWhenReady, int playbackState); @@ -76,6 +77,15 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid void onRenderedFirstFrame(); void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture); boolean onSurfaceDestroyed(SurfaceTexture surfaceTexture); + default void onRenderedFirstFrame(EventTime eventTime) { + + } + default void onSeekStarted(EventTime eventTime) { + + } + default void onSeekFinished(EventTime eventTime) { + + } } public interface AudioVisualizerDelegate { @@ -113,6 +123,7 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid private String videoType, audioType; private boolean loopingMediaSource; private boolean looping; + private int repeatCount; Handler audioUpdateHandler = new Handler(Looper.getMainLooper()); @@ -159,6 +170,7 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid factory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER); player = ExoPlayerFactory.newSimpleInstance(ApplicationLoader.applicationContext, factory, trackSelector, loadControl, null); + player.addAnalyticsListener(this); player.addListener(this); player.setVideoListener(this); if (textureView != null) { @@ -327,6 +339,27 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.playerDidStartPlaying); } + @Override + public void onSeekStarted(EventTime eventTime) { + if (delegate != null) { + delegate.onSeekStarted(eventTime); + } + } + + @Override + public void onSeekProcessed(EventTime eventTime) { + if (delegate != null) { + delegate.onSeekFinished(eventTime); + } + } + + @Override + public void onRenderedFirstFrame(EventTime eventTime, Surface surface) { + if (delegate != null) { + delegate.onRenderedFirstFrame(eventTime); + } + } + public void setTextureView(TextureView texture) { if (textureView == texture) { return; @@ -435,7 +468,7 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid } public boolean isMuted() { - return player.getVolume() == 0.0f; + return player != null && player.getVolume() == 0.0f; } public void setMute(boolean value) { @@ -536,7 +569,7 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { maybeReportPlayerState(); - if (playWhenReady && playbackState == Player.STATE_READY) { + if (playWhenReady && playbackState == Player.STATE_READY && !isMuted()) { NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.playerDidStartPlaying, this); } if (!videoPlayerReady && playbackState == Player.STATE_READY) { @@ -563,7 +596,9 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid @Override public void onPositionDiscontinuity(int reason) { - + if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) { + repeatCount++; + } } @Override @@ -639,6 +674,10 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid } } + public int getRepeatCount() { + return repeatCount; + } + private class AudioVisualizerRenderersFactory extends DefaultRenderersFactory { public AudioVisualizerRenderersFactory(Context context) { @@ -668,11 +707,7 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid @Override public void flush(int sampleRateHz, int channelCount, int encoding) { - if (audioVisualizerDelegate == null) { - return; - } - audioUpdateHandler.removeCallbacksAndMessages(null); - audioVisualizerDelegate.onVisualizerUpdate(false, false, null); + } @@ -757,18 +792,13 @@ public class VideoPlayer implements ExoPlayer.EventListener, SimpleExoPlayer.Vid } int updateInterval = 64; -// if (partsAmplitude[6] >= 0.5f) { -// updateInterval -= 8 * partsAmplitude[6] - 0.5f; -// } if (System.currentTimeMillis() - lastUpdateTime < updateInterval) { return; } lastUpdateTime = System.currentTimeMillis(); - audioUpdateHandler.postDelayed(() -> { - audioVisualizerDelegate.onVisualizerUpdate(true, true, partsAmplitude); - }, 130); + audioUpdateHandler.postDelayed(() -> audioVisualizerDelegate.onVisualizerUpdate(true, true, partsAmplitude), 130); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoSeekPreviewImage.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoSeekPreviewImage.java index fd9f411e1..a6509f710 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoSeekPreviewImage.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoSeekPreviewImage.java @@ -186,10 +186,10 @@ public class VideoSeekPreviewImage extends View { } else { path = FileLoader.getPathToAttach(document, false).getAbsolutePath(); } - fileDrawable = new AnimatedFileDrawable(new File(path), true, document.size, document, parentObject, currentAccount, true); + fileDrawable = new AnimatedFileDrawable(new File(path), true, document.size, document, null, parentObject, 0, currentAccount, true); } else { path = uri.getPath(); - fileDrawable = new AnimatedFileDrawable(new File(path), true, 0, null, null, 0, true); + fileDrawable = new AnimatedFileDrawable(new File(path), true, 0, null, null, null, 0, 0, true); } duration = fileDrawable.getDurationMs(); if (pendingProgress != 0.0f) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelinePlayView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelinePlayView.java index 9410cf48d..3048de566 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelinePlayView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelinePlayView.java @@ -8,7 +8,6 @@ package org.telegram.ui.Components; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -20,6 +19,7 @@ import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.media.MediaMetadataRetriever; import android.os.AsyncTask; +import android.os.Build; import android.view.MotionEvent; import android.view.View; @@ -29,7 +29,6 @@ import org.telegram.messenger.R; import java.util.ArrayList; -@TargetApi(10) public class VideoTimelinePlayView extends View { private long videoLength; @@ -41,7 +40,6 @@ public class VideoTimelinePlayView extends View { private boolean pressedRight; private boolean pressedPlay; private float playProgress = 0.5f; - private float bufferedProgress = 0.5f; private float pressDx; private MediaMetadataRetriever mediaMetadataRetriever; private VideoTimelineViewDelegate delegate; @@ -54,22 +52,30 @@ public class VideoTimelinePlayView extends View { private int framesToLoad; private float maxProgressDiff = 1.0f; private float minProgressDiff = 0.0f; - private boolean isRoundFrames; - private Rect rect1; - private Rect rect2; private RectF rect3 = new RectF(); private Drawable drawableLeft; private Drawable drawableRight; private int lastWidth; + private int currentMode = MODE_VIDEO; + + private ArrayList exclusionRects = new ArrayList<>(); + private android.graphics.Rect exclustionRect = new Rect(); + + public final static int MODE_VIDEO = 0; + public final static int MODE_AVATAR = 1; public interface VideoTimelineViewDelegate { void onLeftProgressChanged(float progress); void onRightProgressChanged(float progress); void onPlayProgressChanged(float progress); - void didStartDragging(); - void didStopDragging(); + void didStartDragging(int type); + void didStopDragging(int type); } + public static int TYPE_LEFT = 0; + public static int TYPE_RIGHT = 1; + public static int TYPE_PROGRESS = 2; + public VideoTimelinePlayView(Context context) { super(context); paint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -80,6 +86,7 @@ public class VideoTimelinePlayView extends View { drawableLeft.setColorFilter(new PorterDuffColorFilter(0xff000000, PorterDuff.Mode.SRC_IN)); drawableRight = context.getResources().getDrawable(R.drawable.video_cropright); drawableRight.setColorFilter(new PorterDuffColorFilter(0xff000000, PorterDuff.Mode.SRC_IN)); + exclusionRects.add(exclustionRect); } public float getProgress() { @@ -98,6 +105,14 @@ public class VideoTimelinePlayView extends View { minProgressDiff = value; } + public void setMode(int mode) { + if (currentMode == mode) { + return; + } + currentMode = mode; + invalidate(); + } + public void setMaxProgressDiff(float value) { maxProgressDiff = value; if (progressRight - progressLeft > maxProgressDiff) { @@ -106,11 +121,12 @@ public class VideoTimelinePlayView extends View { } } - public void setRoundFrames(boolean value) { - isRoundFrames = value; - if (isRoundFrames) { - rect1 = new Rect(AndroidUtilities.dp(14), AndroidUtilities.dp(14), AndroidUtilities.dp(14 + 28), AndroidUtilities.dp(14 + 28)); - rect2 = new Rect(); + @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 >= 29) { + exclustionRect.set(left, 0, right, getMeasuredHeight()); + setSystemGestureExclusionRects(exclusionRects); } } @@ -124,7 +140,7 @@ public class VideoTimelinePlayView extends View { int width = getMeasuredWidth() - AndroidUtilities.dp(32); int startX = (int) (width * progressLeft) + AndroidUtilities.dp(16); - int playX = (int) (width * (progressLeft + (progressRight - progressLeft) * playProgress)) + AndroidUtilities.dp(16); + int playX = (int) (width * playProgress) + AndroidUtilities.dp(16); int endX = (int) (width * progressRight) + AndroidUtilities.dp(16); if (event.getAction() == MotionEvent.ACTION_DOWN) { @@ -136,15 +152,15 @@ public class VideoTimelinePlayView extends View { int additionWidthPlay = AndroidUtilities.dp(8); if (endX != startX && playX - additionWidthPlay <= x && x <= playX + additionWidthPlay && y >= 0 && y <= getMeasuredHeight()) { if (delegate != null) { - delegate.didStartDragging(); + delegate.didStartDragging(TYPE_PROGRESS); } pressedPlay = true; pressDx = (int) (x - playX); invalidate(); return true; - } else if (startX - additionWidth <= x && x <= Math.min(startX + additionWidth,endX) && y >= 0 && y <= getMeasuredHeight()) { + } else if (startX - additionWidth <= x && x <= Math.min(startX + additionWidth, endX) && y >= 0 && y <= getMeasuredHeight()) { if (delegate != null) { - delegate.didStartDragging(); + delegate.didStartDragging(TYPE_LEFT); } pressedLeft = true; pressDx = (int) (x - startX); @@ -152,7 +168,7 @@ public class VideoTimelinePlayView extends View { return true; } else if (endX - additionWidth <= x && x <= endX + additionWidth && y >= 0 && y <= getMeasuredHeight()) { if (delegate != null) { - delegate.didStartDragging(); + delegate.didStartDragging(TYPE_RIGHT); } pressedRight = true; pressDx = (int) (x - endX); @@ -160,11 +176,13 @@ public class VideoTimelinePlayView extends View { return true; } else if (startX <= x && x <= endX && y >= 0 && y <= getMeasuredHeight()) { if (delegate != null) { - delegate.didStartDragging(); + delegate.didStartDragging(TYPE_PROGRESS); } pressedPlay = true; - playX = (int) x; - playProgress = (playX - startX) / (float) (endX - startX); + playProgress = (x - AndroidUtilities.dp(16)) / width; + if (delegate != null) { + delegate.onPlayProgressChanged(playProgress); + } pressDx = 0; invalidate(); return true; @@ -172,19 +190,19 @@ public class VideoTimelinePlayView extends View { } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { if (pressedLeft) { if (delegate != null) { - delegate.didStopDragging(); + delegate.didStopDragging(TYPE_LEFT); } pressedLeft = false; return true; } else if (pressedRight) { if (delegate != null) { - delegate.didStopDragging(); + delegate.didStopDragging(TYPE_RIGHT); } pressedRight = false; return true; } else if (pressedPlay) { if (delegate != null) { - delegate.didStopDragging(); + delegate.didStopDragging(TYPE_PROGRESS); } pressedPlay = false; return true; @@ -198,9 +216,8 @@ public class VideoTimelinePlayView extends View { } else if (playProgress > progressRight) { playProgress = progressRight; } - playProgress = (playProgress - progressLeft) / (progressRight - progressLeft); if (delegate != null) { - delegate.onPlayProgressChanged(progressLeft + (progressRight - progressLeft) * playProgress); + delegate.onPlayProgressChanged(playProgress); } invalidate(); return true; @@ -220,6 +237,11 @@ public class VideoTimelinePlayView extends View { progressLeft = 0; } } + if (progressLeft > playProgress) { + playProgress = progressLeft; + } else if (progressRight < playProgress) { + playProgress = progressRight; + } if (delegate != null) { delegate.onLeftProgressChanged(progressLeft); } @@ -241,6 +263,11 @@ public class VideoTimelinePlayView extends View { progressRight = 1.0f; } } + if (progressLeft > playProgress) { + playProgress = progressLeft; + } else if (progressRight < playProgress) { + playProgress = progressRight; + } if (delegate != null) { delegate.onRightProgressChanged(progressRight); } @@ -248,11 +275,7 @@ public class VideoTimelinePlayView extends View { return true; } } - return false; - } - - public void setColor(int color) { - paint.setColor(color); + return true; } public void setVideoPath(String path, float left, float right) { @@ -270,6 +293,20 @@ public class VideoTimelinePlayView extends View { invalidate(); } + public void setRightProgress(float value) { + progressRight = value; + if (delegate != null) { + delegate.didStartDragging(TYPE_RIGHT); + } + if (delegate != null) { + delegate.onRightProgressChanged(progressRight); + } + if (delegate != null) { + delegate.didStopDragging(TYPE_RIGHT); + } + invalidate(); + } + public void setDelegate(VideoTimelineViewDelegate delegate) { this.delegate = delegate; } @@ -279,14 +316,9 @@ public class VideoTimelinePlayView extends View { return; } if (frameNum == 0) { - if (isRoundFrames) { - frameHeight = frameWidth = AndroidUtilities.dp(56); - framesToLoad = (int) Math.ceil((getMeasuredWidth() - AndroidUtilities.dp(16)) / (frameHeight / 2.0f)); - } else { - frameHeight = AndroidUtilities.dp(40); - framesToLoad = (getMeasuredWidth() - AndroidUtilities.dp(16)) / frameHeight; - frameWidth = (int) Math.ceil((float) (getMeasuredWidth() - AndroidUtilities.dp(16)) / (float) framesToLoad); - } + frameHeight = AndroidUtilities.dp(40); + framesToLoad = (getMeasuredWidth() - AndroidUtilities.dp(16)) / frameHeight; + frameWidth = (int) Math.ceil((float) (getMeasuredWidth() - AndroidUtilities.dp(16)) / (float) framesToLoad); frameTimeOffset = videoLength / framesToLoad; } currentTask = new AsyncTask() { @@ -398,7 +430,7 @@ public class VideoTimelinePlayView extends View { @Override protected void onDraw(Canvas canvas) { - int width = getMeasuredWidth() - AndroidUtilities.dp(36); + int width = getMeasuredWidth() - AndroidUtilities.dp(32); int startX = (int) (width * progressLeft) + AndroidUtilities.dp(16); int endX = (int) (width * progressRight) + AndroidUtilities.dp(16); @@ -411,14 +443,9 @@ public class VideoTimelinePlayView extends View { for (int a = 0; a < frames.size(); a++) { Bitmap bitmap = frames.get(a); if (bitmap != null) { - int x = AndroidUtilities.dp(16) + offset * (isRoundFrames ? frameWidth / 2 : frameWidth); + int x = AndroidUtilities.dp(16) + offset * frameWidth; int y = AndroidUtilities.dp(2 + 4); - if (isRoundFrames) { - rect2.set(x, y, x + AndroidUtilities.dp(28), y + AndroidUtilities.dp(28)); - canvas.drawBitmap(bitmap, rect1, rect2, null); - } else { - canvas.drawBitmap(bitmap, x, y, null); - } + canvas.drawBitmap(bitmap, x, y, null); } offset++; } @@ -446,7 +473,7 @@ public class VideoTimelinePlayView extends View { drawableRight.setBounds(endX + AndroidUtilities.dp(2), AndroidUtilities.dp(4) + (AndroidUtilities.dp(44) - AndroidUtilities.dp(18)) / 2, endX + AndroidUtilities.dp(12), (AndroidUtilities.dp(44) - AndroidUtilities.dp(18)) / 2 + AndroidUtilities.dp(18 + 4)); drawableRight.draw(canvas); - float cx = AndroidUtilities.dp(18) + width * (progressLeft + (progressRight - progressLeft) * playProgress); + float cx = AndroidUtilities.dp(18) + width * playProgress; rect3.set(cx - AndroidUtilities.dp(1.5f), AndroidUtilities.dp(2), cx + AndroidUtilities.dp(1.5f), AndroidUtilities.dp(50)); canvas.drawRoundRect(rect3, AndroidUtilities.dp(1), AndroidUtilities.dp(1), paint2); canvas.drawCircle(cx, AndroidUtilities.dp(52), AndroidUtilities.dp(3.5f), paint2); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/WallpaperUpdater.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/WallpaperUpdater.java index ce92b0eb5..24ac8cb27 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/WallpaperUpdater.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/WallpaperUpdater.java @@ -114,7 +114,7 @@ public class WallpaperUpdater { return; } } - PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(2, false, false, null); + PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(PhotoAlbumPickerActivity.SELECT_TYPE_WALLPAPER, false, false, null); fragment.setAllowSearchImages(false); fragment.setDelegate(new PhotoAlbumPickerActivity.PhotoAlbumPickerActivityDelegate() { @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java index 20a878cc2..5a076f47d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebPlayerView.java @@ -1985,6 +1985,88 @@ public class WebPlayerView extends ViewGroup implements VideoPlayer.VideoPlayerD return id; } + public boolean canHandleUrl(String url) { + if (url != null) { + if (url.endsWith(".mp4")) { + return true; + } else { + try { + Matcher matcher = youtubeIdRegex.matcher(url); + String id = null; + if (matcher.find()) { + id = matcher.group(1); + } + if (id != null) { + return true; + } + } catch (Exception e) { + FileLog.e(e); + } + try { + Matcher matcher = vimeoIdRegex.matcher(url); + String id = null; + if (matcher.find()) { + id = matcher.group(3); + } + if (id != null) { + return true; + } + } catch (Exception e) { + FileLog.e(e); + } + try { + Matcher matcher = aparatIdRegex.matcher(url); + String id = null; + if (matcher.find()) { + id = matcher.group(1); + } + if (id != null) { + return true; + } + } catch (Exception e) { + FileLog.e(e); + } + try { + Matcher matcher = twitchClipIdRegex.matcher(url); + String id = null; + if (matcher.find()) { + id = matcher.group(1); + } + if (id != null) { + return true; + } + } catch (Exception e) { + FileLog.e(e); + } + try { + Matcher matcher = twitchStreamIdRegex.matcher(url); + String id = null; + if (matcher.find()) { + id = matcher.group(1); + } + if (id != null) { + return true; + } + } catch (Exception e) { + FileLog.e(e); + } + try { + Matcher matcher = coubIdRegex.matcher(url); + String id = null; + if (matcher.find()) { + id = matcher.group(1); + } + if (id != null) { + return true; + } + } catch (Exception e) { + FileLog.e(e); + } + } + } + return false; + } + public boolean loadVideo(String url, TLRPC.Photo thumb, Object parentObject, String originalUrl, boolean autoplay) { String youtubeId = null; String vimeoId = null; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ContentPreviewViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/ContentPreviewViewer.java index 01358e5c7..733559a11 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ContentPreviewViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ContentPreviewViewer.java @@ -691,7 +691,7 @@ public class ContentPreviewViewer { } else { if (document != null) { TLRPC.PhotoSize thumb = FileLoader.getClosestPhotoSizeWithSize(document.thumbs, 90); - TLRPC.TL_videoSize videoSize = MessageObject.getDocumentVideoThumb(document); + TLRPC.VideoSize videoSize = MessageObject.getDocumentVideoThumb(document); ImageLocation location = ImageLocation.getForDocument(document); location.imageType = FileLoader.IMAGE_TYPE_ANIMATION; if (videoSize != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DataAutoDownloadActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DataAutoDownloadActivity.java index 3904d4b6c..1c85849e2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DataAutoDownloadActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DataAutoDownloadActivity.java @@ -17,6 +17,7 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.os.Build; import android.text.TextPaint; import android.util.TypedValue; import android.view.Gravity; @@ -755,6 +756,7 @@ public class DataAutoDownloadActivity extends BaseFragment { view.setText(LocaleController.getString("AutoDownloadAudioInfo", R.string.AutoDownloadAudioInfo)); view.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); view.setFixedSize(0); + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } else if (position == autoDownloadSectionRow) { if (usageHeaderRow == -1) { view.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); @@ -765,10 +767,16 @@ public class DataAutoDownloadActivity extends BaseFragment { } else if (currentType == 2) { view.setText(LocaleController.getString("AutoDownloadOnRoamingDataInfo", R.string.AutoDownloadOnRoamingDataInfo)); } + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } else { view.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); view.setText(null); view.setFixedSize(12); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } else { + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } } break; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DataSettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DataSettingsActivity.java index f23d21b4d..cf3d14247 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DataSettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DataSettingsActivity.java @@ -27,6 +27,7 @@ import org.telegram.messenger.MessagesController; import org.telegram.messenger.R; import org.telegram.messenger.SharedConfig; import org.telegram.messenger.voip.TgVoip; +import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BaseFragment; @@ -83,6 +84,8 @@ public class DataSettingsActivity extends BaseFragment { private int proxySectionRow; private int proxyRow; private int proxySection2Row; + private int clearDraftsRow; + private int clearDraftsSectionRow; private int rowCount; @Override @@ -124,6 +127,8 @@ public class DataSettingsActivity extends BaseFragment { proxySectionRow = rowCount++; proxyRow = rowCount++; proxySection2Row = rowCount++; + clearDraftsRow = rowCount++; + clearDraftsSectionRow = rowCount++; return true; } @@ -348,6 +353,21 @@ public class DataSettingsActivity extends BaseFragment { if (view instanceof TextCheckCell) { ((TextCheckCell) view).setChecked(SharedConfig.autoplayVideo); } + } else if (position == clearDraftsRow) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AreYouSureClearDraftsTitle", R.string.AreYouSureClearDraftsTitle)); + builder.setMessage(LocaleController.getString("AreYouSureClearDrafts", R.string.AreYouSureClearDrafts)); + builder.setPositiveButton(LocaleController.getString("Delete", R.string.Delete), (dialogInterface, i) -> { + TLRPC.TL_messages_clearAllDrafts req = new TLRPC.TL_messages_clearAllDrafts(); + getConnectionsManager().sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> getMediaDataController().clearAllDrafts(true))); + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + AlertDialog alertDialog = builder.create(); + showDialog(alertDialog); + TextView button = (TextView) alertDialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (button != null) { + button.setTextColor(Theme.getColor(Theme.key_dialogTextRed2)); + } } }); @@ -384,7 +404,7 @@ public class DataSettingsActivity extends BaseFragment { public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (holder.getItemViewType()) { case 0: { - if (position == proxySection2Row) { + if (position == clearDraftsSectionRow) { holder.itemView.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); } else { holder.itemView.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); @@ -425,6 +445,8 @@ public class DataSettingsActivity extends BaseFragment { textCell.setText(LocaleController.getString("ResetAutomaticMediaDownload", R.string.ResetAutomaticMediaDownload), false); } else if (position == quickRepliesRow){ textCell.setText(LocaleController.getString("VoipQuickReplies", R.string.VoipQuickReplies), false); + } else if (position == clearDraftsRow) { + textCell.setText(LocaleController.getString("PrivacyDeleteCloudDrafts", R.string.PrivacyDeleteCloudDrafts), false); } break; } @@ -564,7 +586,7 @@ public class DataSettingsActivity extends BaseFragment { !controller.mediumPreset.equals(controller.getCurrentMobilePreset()) || controller.mediumPreset.isEnabled() != controller.mobilePreset.enabled || !controller.highPreset.equals(controller.getCurrentWiFiPreset()) || controller.highPreset.isEnabled() != controller.wifiPreset.enabled; } - return position == mobileRow || position == roamingRow || position == wifiRow || position == storageUsageRow || position == useLessDataForCallsRow || position == dataUsageRow || position == proxyRow || + return position == mobileRow || position == roamingRow || position == wifiRow || position == storageUsageRow || position == useLessDataForCallsRow || position == dataUsageRow || position == proxyRow || position == clearDraftsRow || position == enableCacheStreamRow || position == enableStreamRow || position == enableAllStreamRow || position == enableMkvRow || position == quickRepliesRow || position == autoplayVideoRow || position == autoplayGifsRow; } @@ -607,7 +629,7 @@ public class DataSettingsActivity extends BaseFragment { @Override public int getItemViewType(int position) { - if (position == mediaDownloadSection2Row || position == usageSection2Row || position == callsSection2Row || position == proxySection2Row || position == autoplaySectionRow) { + if (position == mediaDownloadSection2Row || position == usageSection2Row || position == callsSection2Row || position == proxySection2Row || position == autoplaySectionRow || position == clearDraftsSectionRow) { return 0; } else if (position == mediaDownloadSectionRow || position == streamSectionRow || position == callsSectionRow || position == usageSectionRow || position == proxySectionRow || position == autoplayHeaderRow) { return 2; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java index 2a2816b11..985baa583 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java @@ -1055,7 +1055,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. @Override public boolean onTouchEvent(MotionEvent e) { - if (scrollAnimationRunning || waitingForScrollFinished || dialogRemoveFinished != 0 || dialogInsertFinished != 0 || dialogChangeFinished != 0) { + if (fastScrollAnimationRunning || waitingForScrollFinished || dialogRemoveFinished != 0 || dialogInsertFinished != 0 || dialogChangeFinished != 0) { return false; } int action = e.getAction(); @@ -1123,7 +1123,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. ValueAnimator valueAnimator = ValueAnimator.ofFloat(getViewOffset(), 0f); valueAnimator.addUpdateListener(animation -> setViewsOffset((float) animation.getAnimatedValue())); - valueAnimator.setDuration((long) (350f - 120f * (getViewOffset() / PullForegroundDrawable.getMaxOverscroll()))); + valueAnimator.setDuration(Math.max(100, (long) (350f - 120f * (getViewOffset() / PullForegroundDrawable.getMaxOverscroll())))); valueAnimator.setInterpolator(CubicBezierInterpolator.EASE_OUT_QUINT); setScrollEnabled(false); valueAnimator.addListener(new AnimatorListenerAdapter() { @@ -1143,7 +1143,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. @Override public boolean onInterceptTouchEvent(MotionEvent e) { - if (scrollAnimationRunning || waitingForScrollFinished || dialogRemoveFinished != 0 || dialogInsertFinished != 0 || dialogChangeFinished != 0) { + if (fastScrollAnimationRunning || waitingForScrollFinished || dialogRemoveFinished != 0 || dialogInsertFinished != 0 || dialogChangeFinished != 0) { return false; } if (e.getAction() == MotionEvent.ACTION_DOWN) { @@ -1457,6 +1457,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. getNotificationCenter().addObserver(this, NotificationCenter.didUpdateConnectionState); getNotificationCenter().addObserver(this, NotificationCenter.needDeleteDialog); getNotificationCenter().addObserver(this, NotificationCenter.folderBecomeEmpty); + getNotificationCenter().addObserver(this, NotificationCenter.newSuggestionsAvailable); NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.didSetPasscode); } @@ -1507,6 +1508,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. getNotificationCenter().removeObserver(this, NotificationCenter.didUpdateConnectionState); getNotificationCenter().removeObserver(this, NotificationCenter.needDeleteDialog); getNotificationCenter().removeObserver(this, NotificationCenter.folderBecomeEmpty); + getNotificationCenter().removeObserver(this, NotificationCenter.newSuggestionsAvailable); NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.didSetPasscode); } @@ -2322,6 +2324,9 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (viewPage.listView.fastScrollAnimationRunning) { + return 0; + } boolean isDragging = viewPage.listView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING; int measuredDy = dy; @@ -2628,7 +2633,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. @Override public void searchStateChanged(boolean search) { if (searching && searchWas && searchEmptyView != null) { - if (search) { + if (search || dialogsSearchAdapter.getItemCount() != 0) { searchEmptyView.showProgress(); } else { searchEmptyView.showTextView(); @@ -3004,7 +3009,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } if (!onlySelect && initialDialogsType == 0) { - blurredView = new ImageView(context); + blurredView = new View(context); blurredView.setVisibility(View.GONE); contentView.addView(blurredView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); } @@ -3291,6 +3296,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } } } + showNextSupportedSuggestion(); } @Override @@ -4546,8 +4552,10 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } } if (canUnarchiveCount != 0) { - archiveItem.setTextAndIcon(LocaleController.getString("Unarchive", R.string.Unarchive), R.drawable.baseline_unarchive_24); + final String contentDescription = LocaleController.getString("Unarchive", R.string.Unarchive); + archiveItem.setTextAndIcon(contentDescription, R.drawable.baseline_unarchive_24); archive2Item.setIcon(R.drawable.baseline_unarchive_24); + archive2Item.setContentDescription(contentDescription); if (filterTabsView != null && filterTabsView.getVisibility() == View.VISIBLE) { archive2Item.setVisibility(View.VISIBLE); archiveItem.setVisibility(View.GONE); @@ -4556,8 +4564,10 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. archive2Item.setVisibility(View.GONE); } } else if (canArchiveCount != 0) { - archiveItem.setTextAndIcon(LocaleController.getString("Archive", R.string.Archive), R.drawable.baseline_archive_24); + final String contentDescription = LocaleController.getString("Archive", R.string.Archive); + archiveItem.setTextAndIcon(contentDescription, R.drawable.baseline_archive_24); archive2Item.setIcon(R.drawable.baseline_archive_24); + archive2Item.setContentDescription(contentDescription); if (filterTabsView != null && filterTabsView.getVisibility() == View.VISIBLE) { archive2Item.setVisibility(View.VISIBLE); archiveItem.setVisibility(View.GONE); @@ -5100,6 +5110,9 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } } else { getMessagesController().deleteDialog(dialogId, 0, revoke); + if (user != null && user.bot) { + getMessagesController().blockUser(user.id); + } } MessagesController.getInstance(currentAccount).checkIfFolderEmpty(folderId); }; @@ -5117,9 +5130,49 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. updateFilterTabs(true); } else if (id == NotificationCenter.filterSettingsUpdated) { showFiltersHint(); + } else if (id == NotificationCenter.newSuggestionsAvailable) { + showNextSupportedSuggestion(); } } + private String showingSuggestion; + private void showNextSupportedSuggestion() { + if (showingSuggestion != null) { + return; + } + for (String suggestion : getMessagesController().pendingSuggestions) { + if (showSuggestion(suggestion)) { + showingSuggestion = suggestion; + return; + } + } + } + + private void onSuggestionDismiss() { + if (showingSuggestion == null) { + return; + } + getMessagesController().removeSuggestion(showingSuggestion); + showingSuggestion = null; + showNextSupportedSuggestion(); + } + + private boolean showSuggestion(String suggestion) { + if ("AUTOARCHIVE_POPULAR".equals(suggestion)) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("HideNewChatsAlertTitle", R.string.HideNewChatsAlertTitle)); + builder.setMessage(AndroidUtilities.replaceTags(LocaleController.getString("HideNewChatsAlertText", R.string.HideNewChatsAlertText))); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + builder.setPositiveButton(LocaleController.getString("GoToSettings", R.string.GoToSettings), (dialog, which) -> { + presentFragment(new PrivacySettingsActivity()); + AndroidUtilities.scrollToFragmentRow(parentLayout, "newChatsRow"); + }); + showDialog(builder.create(), dialog -> onSuggestionDismiss()); + return true; + } + return false; + } + private void showFiltersHint() { if (askingForPermissions || !getMessagesController().dialogFiltersLoaded || !getMessagesController().showFiltersTooltip || filterTabsView == null || filterTabsView.getVisibility() == View.VISIBLE || isPaused || !getUserConfig().filtersLoaded) { return; @@ -5840,8 +5893,6 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. arrayList.add(new ThemeDescription(null, 0, null, null, null, null, Theme.key_player_progressBackground)); arrayList.add(new ThemeDescription(null, 0, null, null, null, null, Theme.key_player_progressCachedBackground)); arrayList.add(new ThemeDescription(null, 0, null, null, null, null, Theme.key_player_progress)); - arrayList.add(new ThemeDescription(null, 0, null, null, null, null, Theme.key_player_placeholder)); - arrayList.add(new ThemeDescription(null, 0, null, null, null, null, Theme.key_player_placeholderBackground)); arrayList.add(new ThemeDescription(null, 0, null, null, null, null, Theme.key_player_button)); arrayList.add(new ThemeDescription(null, 0, null, null, null, null, Theme.key_player_buttonActive)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/FiltersSetupActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/FiltersSetupActivity.java index e64a0f26f..63d447988 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/FiltersSetupActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/FiltersSetupActivity.java @@ -253,6 +253,7 @@ public class FiltersSetupActivity extends BaseFragment implements NotificationCe moveImageView.setScaleType(ImageView.ScaleType.CENTER); moveImageView.setImageResource(R.drawable.list_reorder); moveImageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_stickers_menu), PorterDuff.Mode.SRC_IN)); + moveImageView.setContentDescription(LocaleController.getString("FilterReorder", R.string.FilterReorder)); moveImageView.setClickable(true); addView(moveImageView, LayoutHelper.createFrame(48, 48, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL, 6, 0, 6, 0)); @@ -284,6 +285,7 @@ public class FiltersSetupActivity extends BaseFragment implements NotificationCe optionsImageView.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.getColor(Theme.key_stickers_menuSelector))); optionsImageView.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_stickers_menu), PorterDuff.Mode.SRC_IN)); optionsImageView.setImageResource(R.drawable.msg_actions); + optionsImageView.setContentDescription(LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions)); addView(optionsImageView, LayoutHelper.createFrame(40, 40, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 6, 0, 6, 0)); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java index ddc95cd86..441d119bd 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java @@ -77,6 +77,7 @@ import org.telegram.ui.Cells.TextCell; import org.telegram.ui.Components.CombinedDrawable; import org.telegram.ui.Components.EditTextBoldCursor; import org.telegram.ui.Components.EmptyTextProgressView; +import org.telegram.ui.Components.VerticalPositionAutoAnimator; import org.telegram.ui.Components.GroupCreateDividerItemDecoration; import org.telegram.ui.Components.GroupCreateSpan; import org.telegram.ui.Components.LayoutHelper; @@ -425,6 +426,24 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen }); fragmentView = new ViewGroup(context) { + + private VerticalPositionAutoAnimator verticalPositionAutoAnimator; + + @Override + public void onViewAdded(View child) { + if (child == floatingButton && verticalPositionAutoAnimator == null) { + verticalPositionAutoAnimator = VerticalPositionAutoAnimator.attach(child); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (verticalPositionAutoAnimator != null) { + verticalPositionAutoAnimator.ignoreNextLayout(); + } + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java index 42507cf2b..e3c0e5cde 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java @@ -14,6 +14,7 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.StateListAnimator; import android.annotation.SuppressLint; +import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.graphics.Canvas; @@ -69,6 +70,7 @@ import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RadialProgressView; import org.telegram.ui.Components.RecyclerListView; import org.telegram.ui.Components.SizeNotifierFrameLayout; +import org.telegram.ui.Components.VerticalPositionAutoAnimator; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; @@ -96,7 +98,10 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati private TLRPC.FileLocation avatar; private TLRPC.FileLocation avatarBig; - private TLRPC.InputFile uploadedAvatar; + private TLRPC.InputFile inputPhoto; + private TLRPC.InputFile inputVideo; + private String inputVideoPath; + private double videoTimestamp; private ArrayList selectedContacts; private boolean createAfterUpload; private boolean donePressed; @@ -132,9 +137,9 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.updateInterfaces); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.chatDidCreated); NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.chatDidFailCreate); - imageUpdater = new ImageUpdater(); + imageUpdater = new ImageUpdater(true); imageUpdater.parentFragment = this; - imageUpdater.delegate = this; + imageUpdater.setDelegate(this); selectedContacts = getArguments().getIntegerArrayList("result"); final ArrayList usersToLoad = new ArrayList<>(); for (int a = 0; a < selectedContacts.size(); a++) { @@ -194,6 +199,7 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati if (adapter != null) { adapter.notifyDataSetChanged(); } + imageUpdater.onResume(); AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid); } @@ -203,6 +209,25 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati if (editText != null) { editText.onPause(); } + imageUpdater.onPause(); + } + + @Override + public void dismissCurrentDialog() { + if (imageUpdater.dismissCurrentDialog(visibleDialog)) { + return; + } + super.dismissCurrentDialog(); + } + + @Override + public boolean dismissDialogOnPause(Dialog dialog) { + return imageUpdater.dismissDialogOnPause(dialog) && super.dismissDialogOnPause(dialog); + } + + @Override + public void onRequestPermissionsResultFragment(int requestCode, String[] permissions, int[] grantResults) { + imageUpdater.onRequestPermissionsResultFragment(requestCode, permissions, grantResults); } @Override @@ -214,6 +239,11 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati return true; } + @Override + protected boolean hideKeyboardOnShow() { + return false; + } + @Override public View createView(Context context) { if (editText != null) { @@ -407,7 +437,7 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati protected void onDraw(Canvas canvas) { if (avatarImage != null && avatarProgressView.getVisibility() == VISIBLE) { paint.setAlpha((int) (0x55 * avatarImage.getImageReceiver().getCurrentAlpha() * avatarProgressView.getAlpha())); - canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, AndroidUtilities.dp(32), paint); + canvas.drawCircle(getMeasuredWidth() / 2.0f, getMeasuredHeight() / 2.0f, getMeasuredWidth() / 2.0f, paint); } } }; @@ -415,7 +445,10 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati avatarOverlay.setOnClickListener(view -> imageUpdater.openMenu(avatar != null, () -> { avatar = null; avatarBig = null; - uploadedAvatar = null; + inputPhoto = null; + inputVideo = null; + inputVideoPath = null; + videoTimestamp = 0; showAvatarProgress(false, true); avatarImage.setImage(null, null, avatarDrawable, null); avatarEditor.setImageResource(R.drawable.actions_setphoto); @@ -450,6 +483,7 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati }; avatarProgressView.setSize(AndroidUtilities.dp(30)); avatarProgressView.setProgressColor(0xffffffff); + avatarProgressView.setNoProgress(false); editTextContainer.addView(avatarProgressView, LayoutHelper.createFrame(64, 64, Gravity.TOP | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT), LocaleController.isRTL ? 0 : 16, 16, LocaleController.isRTL ? 16 : 0, 16)); showAvatarProgress(false, false); @@ -523,6 +557,7 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati } }); } + VerticalPositionAutoAnimator.attach(floatingButtonContainer); sizeNotifierFrameLayout.addView(floatingButtonContainer, 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)); floatingButtonContainer.setOnClickListener(view -> { if (donePressed) { @@ -537,7 +572,7 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati AndroidUtilities.hideKeyboard(editText); editText.setEnabled(false); - if (imageUpdater.uploadingImage != null) { + if (imageUpdater.isUploadingImage()) { createAfterUpload = true; } else { showEditDoneProgress(true); @@ -564,10 +599,29 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati } @Override - public void didUploadPhoto(final TLRPC.InputFile file, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { + public void onUploadProgressChanged(float progress) { + if (avatarProgressView == null) { + return; + } + avatarProgressView.setProgress(progress); + } + + @Override + public void didStartUpload(boolean isVideo) { + if (avatarProgressView == null) { + return; + } + avatarProgressView.setProgress(0.0f); + } + + @Override + public void didUploadPhoto(final TLRPC.InputFile photo, final TLRPC.InputFile video, double videoStartTimestamp, String videoPath, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { AndroidUtilities.runOnUIThread(() -> { - if (file != null) { - uploadedAvatar = file; + if (photo != null || video != null) { + inputPhoto = photo; + inputVideo = video; + inputVideoPath = videoPath; + videoTimestamp = videoStartTimestamp; if (createAfterUpload) { if (delegate != null) { delegate.didStartChatCreation(); @@ -728,8 +782,8 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati args2.putInt("chat_id", chat_id); presentFragment(new ChatActivity(args2), true); } - if (uploadedAvatar != null) { - MessagesController.getInstance(currentAccount).changeChatAvatar(chat_id, uploadedAvatar, avatar, avatarBig); + if (inputPhoto != null || inputVideo != null) { + MessagesController.getInstance(currentAccount).changeChatAvatar(chat_id, null, inputPhoto, inputVideo, videoTimestamp, inputVideoPath, avatar, avatarBig); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java index da1f6de67..55de1873c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java @@ -237,7 +237,7 @@ public class IntroActivity extends Activity implements NotificationCenter.Notifi destroyed = true; finish(); }); - if (BuildVars.DEBUG_VERSION) { + if (BuildVars.DEBUG_PRIVATE_VERSION) { startMessagingButton.setOnLongClickListener(v -> { ConnectionsManager.getInstance(currentAccount).switchBackend(); return true; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index 587af380f..ac2aa9389 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -49,6 +49,8 @@ import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.Toast; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -89,12 +91,12 @@ import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.DrawerLayoutAdapter; import org.telegram.ui.Cells.DrawerActionCheckCell; import org.telegram.ui.Cells.DrawerAddCell; +import org.telegram.ui.Cells.DrawerProfileCell; import org.telegram.ui.Cells.DrawerUserCell; import org.telegram.ui.Cells.LanguageCell; import org.telegram.ui.Components.AlertsCreator; import org.telegram.ui.Components.AudioPlayerAlert; import org.telegram.ui.Components.BlockingUpdateView; -import org.telegram.ui.Components.ChatActivityEnterView; import org.telegram.ui.Components.Easings; import org.telegram.ui.Components.EmbedBottomSheet; import org.telegram.ui.Components.JoinGroupAlert; @@ -115,16 +117,11 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Callable; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import kotlin.Unit; -import kotlin.reflect.KFunction; import tw.nekomimi.nekogram.NekoConfig; import tw.nekomimi.nekogram.NekoXConfig; -import tw.nekomimi.nekogram.NekoXSettingActivity; import tw.nekomimi.nekogram.settings.NekoSettingsActivity; import tw.nekomimi.nekogram.sub.SubInfo; import tw.nekomimi.nekogram.sub.SubManager; @@ -539,9 +536,14 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa 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; sideMenu.setLayoutParams(layoutParams); - sideMenu.setOnItemClickListener((view, position) -> { + sideMenu.setOnItemClickListener((view, position, x, y) -> { if (position == 0) { - drawerLayoutAdapter.setAccountsShown(!drawerLayoutAdapter.isAccountsShown(), true); + DrawerProfileCell profileCell = (DrawerProfileCell) view; + if (profileCell.isInAvatar(x, y)) { + openSettings(profileCell.hasAvatar()); + } else { + drawerLayoutAdapter.setAccountsShown(!drawerLayoutAdapter.isAccountsShown(), true); + } } else if (view instanceof DrawerUserCell) { switchToAccount(((DrawerUserCell) view).getAccountNumber(), true); drawerLayoutContainer.closeDrawer(false); @@ -603,7 +605,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa presentFragment(new ProxyListActivity()); drawerLayoutContainer.closeDrawer(false); } else if (id == 14) { - NekoXConfig.toggleKeepOnlineStatus();; + NekoXConfig.toggleKeepOnlineStatus(); + ; drawerLayoutAdapter.notifyDataSetChanged(); } } else { @@ -639,9 +642,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa presentFragment(new InviteContactsActivity()); drawerLayoutContainer.closeDrawer(false); } else if (id == 8) { - SettingsActivity fragment = new SettingsActivity(); - presentFragment(fragment); - drawerLayoutContainer.closeDrawer(false); + openSettings(false); } else if (id == 9) { Browser.openUrl(LaunchActivity.this, NekoXConfig.FAQ_URL); drawerLayoutContainer.closeDrawer(false); @@ -715,7 +716,8 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } break; case "settings": { - SettingsActivity settings = new SettingsActivity(); + args.putInt("user_id", UserConfig.getInstance(currentAccount).clientUserId); + ProfileActivity settings = new ProfileActivity(args); actionBarLayout.addFragmentToStack(settings); settings.restoreSelfArgs(savedInstanceState); break; @@ -841,6 +843,17 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa }); } + private void openSettings(boolean expanded) { + Bundle args = new Bundle(); + args.putInt("user_id", UserConfig.getInstance(currentAccount).clientUserId); + if (expanded) { + args.putBoolean("expandPhoto", true); + } + ProfileActivity fragment = new ProfileActivity(args); + presentFragment(fragment); + drawerLayoutContainer.closeDrawer(false); + } + private void checkSystemBarColors() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { int color = Theme.getColor(Theme.key_actionBarDefault, null, true); @@ -1158,13 +1171,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa int open_settings = 0; int open_new_dialog = 0; long dialogId = 0; - if (SharedConfig.directShare && intent != null && intent.getExtras() != null) { - dialogId = intent.getExtras().getLong("dialogId", 0); - long hash = intent.getExtras().getLong("hash", 0); - if (hash != SharedConfig.directShareHash) { - dialogId = 0; - } - } boolean showDialogsList = false; boolean showPlayer = false; boolean showLocations = false; @@ -1183,6 +1189,35 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if ((flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) { if (intent != null && intent.getAction() != null && !restore) { if (Intent.ACTION_SEND.equals(intent.getAction())) { + if (SharedConfig.directShare && intent != null && intent.getExtras() != null) { + dialogId = intent.getExtras().getLong("dialogId", 0); + String hash = null; + if (dialogId == 0) { + try { + String id = intent.getExtras().getString(ShortcutManagerCompat.EXTRA_SHORTCUT_ID); + if (id != null) { + List list = ShortcutManagerCompat.getDynamicShortcuts(ApplicationLoader.applicationContext); + for (int a = 0, N = list.size(); a < N; a++) { + ShortcutInfoCompat info = list.get(a); + if (id.equals(info.getId())) { + Bundle extras = info.getIntent().getExtras(); + dialogId = extras.getLong("dialogId", 0); + hash = extras.getString("hash", null); + break; + } + } + } + } catch (Throwable e) { + FileLog.e(e); + } + } else { + hash = intent.getExtras().getString("hash", null); + } + if (SharedConfig.directShareHash == null || !SharedConfig.directShareHash.equals(hash)) { + dialogId = 0; + } + } + boolean error = false; String type = intent.getType(); if (type != null && type.equals(ContactsContract.Contacts.CONTENT_VCARD_TYPE)) { @@ -1737,28 +1772,11 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa open_settings = 3; } else if (url.contains("folders")) { open_settings = 4; - } else if (url.contains("nekox")) { - open_settings = 101; + } else if (url.contains("change_number")) { + open_settings = 5; } else if (url.contains("neko")) { open_settings = 100; } - } else if (url.startsWith("tg:meow") || url.startsWith("tg://meow") || url.startsWith("tg:nya") || url.startsWith("tg://nya") || url.startsWith("tg:miao") || url.startsWith("tg://miao")) { - try { - Toast.makeText(LaunchActivity.this, LocaleController.getString("Nya", R.string.Nya), Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - FileLog.e(e); - } - } else if (url.startsWith("tg:user") || url.startsWith("tg://user")) { - try { - url = url.replace("tg:user", "tg://telegram.org").replace("tg://user", "tg://telegram.org"); - data = Uri.parse(url); - int userId = Utilities.parseInt(data.getQueryParameter("id")); - if (userId != 0) { - push_user_id = userId; - } - } catch (Exception e) { - FileLog.e(e); - } } else { unsupportedUrl = url.replace("tg://", "").replace("tg:", ""); int index; @@ -1947,26 +1965,27 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } else if (open_settings != 0) { BaseFragment fragment; + boolean closePrevious = false; if (open_settings == 1) { - fragment = new SettingsActivity(); + Bundle args = new Bundle(); + args.putInt("user_id", UserConfig.getInstance(currentAccount).clientUserId); + fragment = new ProfileActivity(args); } else if (open_settings == 2) { fragment = new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC); } else if (open_settings == 3) { fragment = new SessionsActivity(0); } else if (open_settings == 4) { fragment = new FiltersSetupActivity(); + } else if (open_settings == 5) { + fragment = new ActionIntroActivity(ActionIntroActivity.ACTION_TYPE_CHANGE_PHONE_NUMBER); + closePrevious = true; } else if (open_settings == 100) { fragment = new NekoSettingsActivity(); - } else if (open_settings == 101) { - if (NekoXConfig.developerMode) { - fragment = new NekoXSettingActivity(); - } else { - fragment = new NekoSettingsActivity(); - } } else { fragment = null; } - AndroidUtilities.runOnUIThread(() -> presentFragment(fragment)); + boolean closePreviousFinal = closePrevious; + AndroidUtilities.runOnUIThread(() -> presentFragment(fragment, closePreviousFinal, false)); if (AndroidUtilities.isTablet()) { actionBarLayout.showLastFragment(); rightActionBarLayout.showLastFragment(); @@ -2251,7 +2270,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa boolean hideProgressDialog = true; if (error == null && actionBarLayout != null) { TLRPC.ChatInvite invite = (TLRPC.ChatInvite) response; - if (invite.chat != null && (!ChatObject.isLeftFromChat(invite.chat) || !invite.chat.kicked && (!TextUtils.isEmpty(invite.chat.username) || BuildVars.DEBUG_PRIVATE_VERSION))) { + if (invite.chat != null && (!ChatObject.isLeftFromChat(invite.chat) || !invite.chat.kicked && (!TextUtils.isEmpty(invite.chat.username) || invite instanceof TLRPC.TL_chatInvitePeek))) { MessagesController.getInstance(intentAccount).putChat(invite.chat, false); ArrayList chats = new ArrayList<>(); chats.add(invite.chat); @@ -2272,6 +2291,9 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa return; } ChatActivity fragment = new ChatActivity(args); + if (invite instanceof TLRPC.TL_chatInvitePeek) { + fragment.setChatInvite(invite); + } actionBarLayout.presentFragment(fragment); }, () -> { if (!LaunchActivity.this.isFinishing()) { @@ -2366,13 +2388,15 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa TLRPC.TL_inputStickerSetShortName stickerset = new TLRPC.TL_inputStickerSetShortName(); stickerset.short_name = sticker; BaseFragment fragment = mainFragmentsStack.get(mainFragmentsStack.size() - 1); - ChatActivityEnterView delegate; + StickersAlert alert; if (fragment instanceof ChatActivity) { - delegate = ((ChatActivity) fragment).getChatActivityEnterView(); + ChatActivity chatActivity = (ChatActivity) fragment; + alert = new StickersAlert(LaunchActivity.this, fragment, stickerset, null, chatActivity.getChatActivityEnterView()); + alert.setCalcMandatoryInsets(chatActivity.isKeyboardVisible()); } else { - delegate = null; + alert = new StickersAlert(LaunchActivity.this, fragment, stickerset, null, null); } - fragment.showDialog(new StickersAlert(LaunchActivity.this, fragment, stickerset, null, delegate)); + fragment.showDialog(alert); } return; } else if (message != null) { @@ -3319,7 +3343,13 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } } else if (id == NotificationCenter.reloadInterface) { - boolean last = mainFragmentsStack.size() > 1 && mainFragmentsStack.get(mainFragmentsStack.size() - 1) instanceof SettingsActivity; + boolean last = mainFragmentsStack.size() > 1 && mainFragmentsStack.get(mainFragmentsStack.size() - 1) instanceof ProfileActivity; + if (last) { + ProfileActivity profileActivity = (ProfileActivity) mainFragmentsStack.get(mainFragmentsStack.size() - 1); + if (!profileActivity.isSettings()) { + last = false; + } + } rebuildAllFragments(last); } else if (id == NotificationCenter.suggestedLangpack) { showLanguageAlert(false); @@ -3870,16 +3900,19 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (lastFragment instanceof ChatActivity && args != null) { outState.putBundle("args", args); outState.putString("fragment", "chat"); - } else if (lastFragment instanceof SettingsActivity) { - outState.putString("fragment", "settings"); } else if (lastFragment instanceof GroupCreateFinalActivity && args != null) { outState.putBundle("args", args); outState.putString("fragment", "group"); } else if (lastFragment instanceof WallpapersListActivity) { outState.putString("fragment", "wallpapers"); - } else if (lastFragment instanceof ProfileActivity && ((ProfileActivity) lastFragment).isChat() && args != null) { - outState.putBundle("args", args); - outState.putString("fragment", "chat_profile"); + } else if (lastFragment instanceof ProfileActivity) { + ProfileActivity profileActivity = (ProfileActivity) lastFragment; + if (profileActivity.isSettings()) { + outState.putString("fragment", "settings"); + } else if (profileActivity.isChat() && args != null) { + outState.putBundle("args", args); + outState.putString("fragment", "chat_profile"); + } } else if (lastFragment instanceof ChannelCreateActivity && args != null && args.getInt("step") == 0) { outState.putBundle("args", args); outState.putString("fragment", "channel"); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java index 7d76d304f..1c605ae23 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java @@ -104,6 +104,7 @@ import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.CombinedDrawable; import org.telegram.ui.Components.ContextProgressView; import org.telegram.ui.Components.EditTextBoldCursor; +import org.telegram.ui.Components.VerticalPositionAutoAnimator; import org.telegram.ui.Components.HintEditText; import org.telegram.ui.Components.ImageUpdater; import org.telegram.ui.Components.LayoutHelper; @@ -942,59 +943,7 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No doneItem.setContentDescription(LocaleController.getString("Done", R.string.Done)); doneItem.setVisibility(doneButtonVisible[DONE_TYPE_ACTION] ? View.VISIBLE : View.GONE); - FrameLayout container = new FrameLayout(context) { - - private ObjectAnimator floatingButtonAnimator; - private ObjectAnimator privacyViewAnimator; - - @Override - public void onViewAdded(View child) { - if (child == floatingButtonContainer && floatingButtonAnimator == null) { - floatingButtonAnimator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, 0f); - floatingButtonAnimator.setInterpolator(AndroidUtilities.decelerateInterpolator); - floatingButtonAnimator.setStartDelay(150); - floatingButtonAnimator.setDuration(200); - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - if (privacyViewAnimator == null) { - final TextView privacyView = ((LoginActivityRegisterView) views[5]).privacyView; - privacyViewAnimator = ObjectAnimator.ofFloat(privacyView, View.TRANSLATION_Y, 0f); - privacyViewAnimator.setInterpolator(AndroidUtilities.decelerateInterpolator); - privacyViewAnimator.setStartDelay(150); - privacyViewAnimator.setDuration(200); - } - - if (oldh == 0 || h == oldh || pagesAnimation != null && pagesAnimation.isRunning()) { - return; - } - - final float marginBottom = AndroidUtilities.dp(16f); - - if (floatingButtonAnimator != null) { - final float yOffset = floatingButtonContainer.getTranslationY() + oldh - h; - final float viewHeight = floatingButtonContainer.getHeight() + marginBottom; - final float translationY = Math.min(yOffset, viewHeight); - floatingButtonAnimator.cancel(); - floatingButtonContainer.setTranslationY(translationY); - floatingButtonAnimator.setFloatValues(translationY, 0f); - floatingButtonAnimator.start(); - } - - if (currentViewNum == 5) { - final TextView privacyView = ((LoginActivityRegisterView) views[5]).privacyView; - final float yOffset = privacyView.getTranslationY() + oldh - h; - final float viewHeight = privacyView.getHeight() + marginBottom; - final float translationY = Math.min(yOffset, viewHeight); - privacyViewAnimator.cancel(); - privacyView.setTranslationY(translationY); - privacyViewAnimator.setFloatValues(translationY, 0f); - privacyViewAnimator.start(); - } - } - }; + FrameLayout container = new FrameLayout(context); fragmentView = container; ScrollView scrollView = new ScrollView(context) { @@ -1078,6 +1027,7 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No } }); } + VerticalPositionAutoAnimator.attach(floatingButtonContainer); container.addView(floatingButtonContainer, LayoutHelper.createFrame(Build.VERSION.SDK_INT >= 21 ? 56 : 60, Build.VERSION.SDK_INT >= 21 ? 56 : 60, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 14, 14)); floatingButtonContainer.setOnClickListener(view -> onDoneButtonPressed()); @@ -2764,10 +2714,8 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No int maxHeight = AndroidUtilities.dp(291); if (scrollHeight - innerHeight < requiredHeight) { setMeasuredDimension(getMeasuredWidth(), innerHeight + requiredHeight); - } else if (scrollHeight > maxHeight) { - setMeasuredDimension(getMeasuredWidth(), maxHeight); } else { - setMeasuredDimension(getMeasuredWidth(), scrollHeight); + setMeasuredDimension(getMeasuredWidth(), Math.min(scrollHeight, maxHeight)); } } } @@ -4222,7 +4170,6 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No private TLRPC.FileLocation avatar; private TLRPC.FileLocation avatarBig; - private TLRPC.InputFile uploadedAvatar; private boolean createAfterUpload; @@ -4281,11 +4228,12 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No setOrientation(VERTICAL); - imageUpdater = new ImageUpdater(); + imageUpdater = new ImageUpdater(false); + imageUpdater.setOpenWithFrontfaceCamera(true); imageUpdater.setSearchAvailable(false); imageUpdater.setUploadAfterSelect(false); imageUpdater.parentFragment = LoginActivity.this; - imageUpdater.delegate = this; + imageUpdater.setDelegate(this); textView = new TextView(context); textView.setText(LocaleController.getString("RegisterText2", R.string.RegisterText2)); @@ -4329,7 +4277,7 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No protected void onDraw(Canvas canvas) { if (avatarImage != null && avatarProgressView.getVisibility() == VISIBLE) { paint.setAlpha((int) (0x55 * avatarImage.getImageReceiver().getCurrentAlpha() * avatarProgressView.getAlpha())); - canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, AndroidUtilities.dp(32), paint); + canvas.drawCircle(getMeasuredWidth() / 2.0f, getMeasuredHeight() / 2.0f, getMeasuredWidth() / 2.0f, paint); } } }; @@ -4337,7 +4285,6 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No avatarOverlay.setOnClickListener(view -> imageUpdater.openMenu(avatar != null, () -> { avatar = null; avatarBig = null; - uploadedAvatar = null; showAvatarProgress(false, true); avatarImage.setImage(null, null, avatarDrawable, null); avatarEditor.setImageResource(R.drawable.actions_setphoto); @@ -4445,6 +4392,7 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No privacyView.setLinkTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteLinkText)); privacyView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); privacyView.setLineSpacing(AndroidUtilities.dp(2), 1.0f); + VerticalPositionAutoAnimator.attach(privacyView); privacyLayout.addView(privacyView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.BOTTOM)); String str = LocaleController.getString("TermsOfServiceLogin", R.string.TermsOfServiceLogin); @@ -4460,7 +4408,7 @@ public class LoginActivity extends BaseFragment implements NotificationCenter.No } @Override - public void didUploadPhoto(final TLRPC.InputFile file, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { + public void didUploadPhoto(final TLRPC.InputFile photo, final TLRPC.InputFile video, double videoStartTimestamp, String videoPath, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { AndroidUtilities.runOnUIThread(() -> { avatar = smallSize.location; avatarBig = bigSize.location; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java index 0f54544dd..ae84bdc97 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java @@ -345,7 +345,7 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No MediaController.getInstance().setVoiceMessagesPlaylist(result ? sharedMediaData[MediaDataController.MEDIA_MUSIC].messages : null, false); return result; } else if (messageObject.isMusic()) { - return MediaController.getInstance().setPlaylist(sharedMediaData[MediaDataController.MEDIA_MUSIC].messages, messageObject); + return MediaController.getInstance().setPlaylist(sharedMediaData[MediaDataController.MEDIA_MUSIC].messages, messageObject, mergeDialogId); } return false; } @@ -2082,7 +2082,7 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No } private void openWebView(TLRPC.WebPage webPage) { - EmbedBottomSheet.show(getParentActivity(), webPage.site_name, webPage.description, webPage.url, webPage.embed_url, webPage.embed_width, webPage.embed_height); + EmbedBottomSheet.show(getParentActivity(), webPage.site_name, webPage.description, webPage.url, webPage.embed_url, webPage.embed_width, webPage.embed_height, false); } private void recycleAdapter(RecyclerView.Adapter adapter) { @@ -2372,7 +2372,7 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No MediaController.getInstance().setVoiceMessagesPlaylist(result ? sharedMediaData[currentType].messages : null, false); return result; } else if (messageObject.isMusic()) { - return MediaController.getInstance().setPlaylist(sharedMediaData[currentType].messages, messageObject); + return MediaController.getInstance().setPlaylist(sharedMediaData[currentType].messages, messageObject, mergeDialogId); } return false; } @@ -2870,7 +2870,7 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No } return result; } else if (messageObject.isMusic()) { - return MediaController.getInstance().setPlaylist(searchResult, messageObject); + return MediaController.getInstance().setPlaylist(searchResult, messageObject, mergeDialogId); } return false; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PassportActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PassportActivity.java index 1b145e28d..850b755d5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PassportActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PassportActivity.java @@ -1123,7 +1123,7 @@ public class PassportActivity extends BaseFragment implements NotificationCenter spanned.setSpan(new URLSpanNoUnderline(LocaleController.getString("PassportInfoUrl", R.string.PassportInfoUrl)) { @Override public void onClick(View widget) { - dismissCurrentDialig(); + dismissCurrentDialog(); super.onClick(widget); } }, index1, index2 - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -1342,14 +1342,14 @@ public class PassportActivity extends BaseFragment implements NotificationCenter } @Override - public void dismissCurrentDialig() { + public void dismissCurrentDialog() { if (chatAttachAlert != null && visibleDialog == chatAttachAlert) { chatAttachAlert.getPhotoLayout().closeCamera(false); chatAttachAlert.dismissInternal(); chatAttachAlert.getPhotoLayout().hideCamera(true); return; } - super.dismissCurrentDialig(); + super.dismissCurrentDialog(); } private String getTranslitString(String value) { @@ -6601,7 +6601,7 @@ public class PassportActivity extends BaseFragment implements NotificationCenter @Override public void onRequestPermissionsResultFragment(int requestCode, String[] permissions, int[] grantResults) { if ((currentActivityType == TYPE_IDENTITY || currentActivityType == TYPE_ADDRESS) && chatAttachAlert != null) { - if (requestCode == 17 && chatAttachAlert != null) { + if (requestCode == 17) { chatAttachAlert.getPhotoLayout().checkCamera(false); } else if (requestCode == 21) { if (getParentActivity() == null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PeopleNearbyActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PeopleNearbyActivity.java index 9f31b25b5..ad9eabbfb 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PeopleNearbyActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PeopleNearbyActivity.java @@ -33,6 +33,7 @@ import org.telegram.messenger.ChatObject; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; import org.telegram.messenger.LocationController; +import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.UserConfig; @@ -55,6 +56,7 @@ import org.telegram.ui.Components.UndoView; import java.util.ArrayList; +import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -124,16 +126,17 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe private int chatsCreateRow; private int chatsSectionRow; private int rowCount; + private DefaultItemAnimator itemAnimator; public PeopleNearbyActivity() { super(); users = new ArrayList<>(getLocationController().getCachedNearbyUsers()); chats = new ArrayList<>(getLocationController().getCachedNearbyChats()); checkForExpiredLocations(false); - updateRows(); + updateRows(true); } - private void updateRows() { + private void updateRows(boolean notifyDataSetChanged) { rowCount = 0; usersStartRow = -1; usersEndRow = -1; @@ -172,7 +175,8 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe } chatsSectionRow = rowCount++; - if (listViewAdapter != null) { + if (notifyDataSetChanged && listViewAdapter != null) { + listView.setItemAnimator(null); listViewAdapter.notifyDataSetChanged(); } } @@ -262,6 +266,12 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe listView.setVerticalScrollbarPosition(LocaleController.isRTL ? RecyclerListView.SCROLLBAR_POSITION_LEFT : RecyclerListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + itemAnimator = new DefaultItemAnimator() { + @Override + protected long getAddAnimationDelay(long removeDuration, long moveDuration, long changeDuration) { + return removeDuration; + } + }; listView.setOnItemClickListener((view, position) -> { if (getParentActivity() == null) { return; @@ -275,6 +285,8 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe if (cell.hasAvatarSet()) { args1.putBoolean("expandPhoto", true); } + args1.putInt("nearby_distance", peerLocated.distance); + MessagesController.getInstance(currentAccount).ensureMessagesLoaded(peerLocated.peer.user_id, false, 0, null, null); presentFragment(new ProfileActivity(args1)); } } else if (position >= chatsStartRow && position < chatsEndRow) { @@ -303,7 +315,7 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe userConfig.sharingMyLocationUntil = 0; userConfig.saveConfig(false); sendRequest(false, 2); - updateRows(); + updateRows(true); } else { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setTitle(LocaleController.getString("MakeMyselfVisibleTitle", R.string.MakeMyselfVisibleTitle)); @@ -312,15 +324,19 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe userConfig.sharingMyLocationUntil = 0x7fffffff; userConfig.saveConfig(false); sendRequest(false, 1); - updateRows(); + updateRows(true); }); builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); showDialog(builder.create()); } userConfig.saveConfig(false); } else if (position == showMoreRow) { + int newCount = users.size() - Math.min(5, users.size()); expanded = true; - updateRows(); + updateRows(false); + listView.setItemAnimator(itemAnimator); + listViewAdapter.notifyItemRemoved(position); + listViewAdapter.notifyItemRangeInserted(position, newCount); } }); listView.setOnScrollListener(new RecyclerView.OnScrollListener() { @@ -349,7 +365,7 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe undoView = new UndoView(context); frameLayout.addView(undoView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT, 8, 0, 8, 8)); - updateRows(); + updateRows(true); return fragmentView; } @@ -524,7 +540,7 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe if (share == 1 && error != null) { userConfig.sharingMyLocationUntil = 0; saveConfig = true; - updateRows(); + updateRows(true); } if (response != null && share != 2) { TLRPC.Updates updates = (TLRPC.TL_updates) response; @@ -567,7 +583,7 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe } checkForExpiredLocations(true); - updateRows(); + updateRows(true); } if (saveConfig) { userConfig.saveConfig(false); @@ -661,7 +677,7 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe } } checkForExpiredLocations(true); - updateRows(); + updateRows(true); } else if (id == NotificationCenter.needDeleteDialog) { if (fragmentView == null || isPaused) { return; @@ -712,7 +728,7 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe } } if (changed && listViewAdapter != null) { - updateRows(); + updateRows(true); } if (changed || cache) { getLocationController().setCachedNearbyUsersAndChats(users, chats); @@ -848,7 +864,7 @@ public class PeopleNearbyActivity extends BaseFragment implements NotificationCe } private String formatDistance(TLRPC.TL_peerLocated located) { - return LocaleController.formatDistance(located.distance); + return LocaleController.formatDistance(located.distance, 0); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java index 5f4b85570..a78b76790 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java @@ -31,6 +31,8 @@ import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; @@ -130,6 +132,12 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati private PhotoAlbumPickerActivityDelegate delegate; + public static int SELECT_TYPE_ALL = 0; + public static int SELECT_TYPE_AVATAR = 1; + public static int SELECT_TYPE_WALLPAPER = 2; + public static int SELECT_TYPE_AVATAR_VIDEO = 3; + public static int SELECT_TYPE_QR = 10; + public PhotoAlbumPickerActivity(int selectPhotoType, boolean allowGifs, boolean allowCaption, ChatActivity chatActivity) { super(); this.chatActivity = chatActivity; @@ -140,7 +148,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati @Override public boolean onFragmentCreate() { - if (selectPhotoType != 0 || !allowSearchImages) { + if (selectPhotoType == SELECT_TYPE_AVATAR || selectPhotoType == SELECT_TYPE_WALLPAPER || selectPhotoType == SELECT_TYPE_QR || !allowSearchImages) { albumsSorted = MediaController.allPhotoAlbums; } else { albumsSorted = MediaController.allMediaAlbums; @@ -382,12 +390,22 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati commentTextView.setText(caption); } - writeButtonContainer = new FrameLayout(context); + writeButtonContainer = new FrameLayout(context) { + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setText(LocaleController.formatPluralString("AccDescrSendPhotos", selectedPhotos.size())); + info.setClassName(Button.class.getName()); + info.setLongClickable(true); + info.setClickable(true); + } + }; + writeButtonContainer.setFocusable(true); + writeButtonContainer.setFocusableInTouchMode(true); writeButtonContainer.setVisibility(View.INVISIBLE); writeButtonContainer.setScaleX(0.2f); writeButtonContainer.setScaleY(0.2f); writeButtonContainer.setAlpha(0.0f); - writeButtonContainer.setContentDescription(LocaleController.getString("Send", R.string.Send)); sizeNotifierFrameLayout.addView(writeButtonContainer, LayoutHelper.createFrame(60, 60, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 12, 10)); writeButton = new ImageView(context); @@ -401,6 +419,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati } writeButton.setBackgroundDrawable(writeButtonDrawable); writeButton.setImageResource(R.drawable.attach_send); + writeButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); writeButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingIcon), PorterDuff.Mode.SRC_IN)); writeButton.setScaleType(ImageView.ScaleType.CENTER); if (Build.VERSION.SDK_INT >= 21) { @@ -481,7 +500,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati } itemCells[a].setMinimumWidth(AndroidUtilities.dp(196)); - sendPopupLayout.addView(itemCells[a], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a, 0, 0)); + sendPopupLayout.addView(itemCells[a], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); int chatId; if (chat != null) { @@ -491,7 +510,6 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati } else { chatId = -1; } - itemCells[a].setOnClickListener(v -> { if (sendPopupWindow != null && sendPopupWindow.isShowing()) { sendPopupWindow.dismiss(); @@ -523,6 +541,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati return false; }); } + sendPopupLayout.setupRadialSelectors(Theme.getColor(Theme.key_dialogButtonSelector)); sendPopupWindow = new ActionBarPopupWindow(sendPopupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT); sendPopupWindow.setAnimationEnabled(false); @@ -575,7 +594,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati selectedCountView.setScaleX(0.2f); selectedCountView.setScaleY(0.2f); sizeNotifierFrameLayout.addView(selectedCountView, LayoutHelper.createFrame(42, 24, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, -2, 9)); - if (selectPhotoType != 0) { + if (selectPhotoType != SELECT_TYPE_ALL) { commentTextView.setVisibility(View.GONE); } @@ -668,7 +687,7 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati if (id == NotificationCenter.albumsDidLoad) { int guid = (Integer) args[0]; if (classGuid == guid) { - if (selectPhotoType != 0 || !allowSearchImages) { + if (selectPhotoType == SELECT_TYPE_AVATAR || selectPhotoType == SELECT_TYPE_WALLPAPER || selectPhotoType == SELECT_TYPE_QR || !allowSearchImages) { albumsSorted = (ArrayList) args[2]; } else { albumsSorted = (ArrayList) args[1]; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java index ee6670af0..538ba5b6f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java @@ -38,8 +38,10 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.DecelerateInterpolator; import android.view.animation.OvershootInterpolator; +import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; @@ -818,11 +820,11 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen onListItemClick(view, arrayList.get(position)); } else { int type; - if (selectPhotoType == 1) { + if (selectPhotoType == PhotoAlbumPickerActivity.SELECT_TYPE_AVATAR || selectPhotoType == PhotoAlbumPickerActivity.SELECT_TYPE_AVATAR_VIDEO) { type = PhotoViewer.SELECT_TYPE_AVATAR; - } else if (selectPhotoType == 2) { + } else if (selectPhotoType == PhotoAlbumPickerActivity.SELECT_TYPE_WALLPAPER) { type = PhotoViewer.SELECT_TYPE_WALLPAPER; - } else if (selectPhotoType == 10) { + } else if (selectPhotoType == PhotoAlbumPickerActivity.SELECT_TYPE_QR) { type = PhotoViewer.SELECT_TYPE_QR; } else if (chatActivity == null) { type = 4; @@ -834,18 +836,20 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen PhotoViewer.getInstance().openPhotoForSelect(arrayList, position, type, isDocumentsPicker, provider, chatActivity); } }); - listView.setOnItemLongClickListener((view, position) -> { - if (listSort) { - onListItemClick(view, selectedAlbum.photos.get(position)); - return true; - } else { - if (view instanceof PhotoAttachPhotoCell) { - PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) view; - itemRangeSelector.setIsActive(view, true, position, shouldSelect = !cell.isChecked()); + if (maxSelectedPhotos != 1) { + listView.setOnItemLongClickListener((view, position) -> { + if (listSort) { + onListItemClick(view, selectedAlbum.photos.get(position)); + return true; + } else { + if (view instanceof PhotoAttachPhotoCell) { + PhotoAttachPhotoCell cell = (PhotoAttachPhotoCell) view; + itemRangeSelector.setIsActive(view, true, position, shouldSelect = !cell.isChecked()); + } } - } - return false; - }); + return false; + }); + } itemRangeSelector = new RecyclerViewItemRangeSelector(new RecyclerViewItemRangeSelector.RecyclerViewItemRangeSelectorDelegate() { @Override public int getItemCount() { @@ -888,7 +892,9 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen listView.hideSelector(true); } }); - listView.addOnItemTouchListener(itemRangeSelector); + if (maxSelectedPhotos != 1) { + listView.addOnItemTouchListener(itemRangeSelector); + } emptyView = new EmptyTextProgressView(context); emptyView.setTextColor(0xff93999d); @@ -901,7 +907,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen emptyView.setPadding(0, AndroidUtilities.dp(200), 0, 0); emptyView.setText(LocaleController.getString("NoRecentSearches", R.string.NoRecentSearches)); } - sizeNotifierFrameLayout.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, selectPhotoType != 0 ? 0 : 48)); + sizeNotifierFrameLayout.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, selectPhotoType != PhotoAlbumPickerActivity.SELECT_TYPE_ALL ? 0 : 48)); listView.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -980,12 +986,22 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen } }); - writeButtonContainer = new FrameLayout(context); + writeButtonContainer = new FrameLayout(context) { + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setText(LocaleController.formatPluralString("AccDescrSendPhotos", selectedPhotos.size())); + info.setClassName(Button.class.getName()); + info.setLongClickable(true); + info.setClickable(true); + } + }; + writeButtonContainer.setFocusable(true); + writeButtonContainer.setFocusableInTouchMode(true); writeButtonContainer.setVisibility(View.INVISIBLE); writeButtonContainer.setScaleX(0.2f); writeButtonContainer.setScaleY(0.2f); writeButtonContainer.setAlpha(0.0f); - writeButtonContainer.setContentDescription(LocaleController.getString("Send", R.string.Send)); sizeNotifierFrameLayout.addView(writeButtonContainer, LayoutHelper.createFrame(60, 60, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 12, 10)); writeButton = new ImageView(context); @@ -999,6 +1015,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen } writeButton.setBackgroundDrawable(writeButtonDrawable); writeButton.setImageResource(R.drawable.attach_send); + writeButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); writeButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingIcon), PorterDuff.Mode.SRC_IN)); writeButton.setScaleType(ImageView.ScaleType.CENTER); if (Build.VERSION.SDK_INT >= 21) { @@ -1075,7 +1092,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen } itemCells[a].setMinimumWidth(AndroidUtilities.dp(196)); - sendPopupLayout.addView(itemCells[a], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * a, 0, 0)); + sendPopupLayout.addView(itemCells[a], LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); int chatId; if (chat != null) { chatId = chat.id; @@ -1111,6 +1128,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen return false; }); } + sendPopupLayout.setupRadialSelectors(Theme.getColor(Theme.key_dialogButtonSelector)); sendPopupWindow = new ActionBarPopupWindow(sendPopupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT); sendPopupWindow.setAnimationEnabled(false); @@ -1163,7 +1181,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen selectedCountView.setScaleX(0.2f); selectedCountView.setScaleY(0.2f); sizeNotifierFrameLayout.addView(selectedCountView, LayoutHelper.createFrame(42, 24, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, -2, 9)); - if (selectPhotoType != 0) { + if (selectPhotoType != PhotoAlbumPickerActivity.SELECT_TYPE_ALL) { commentTextView.setVisibility(View.GONE); } } @@ -1786,7 +1804,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen applyCaption(); sendPressed = true; delegate.actionButtonPressed(false, notify, scheduleDate); - if (selectPhotoType != 2) { + if (selectPhotoType != PhotoAlbumPickerActivity.SELECT_TYPE_WALLPAPER) { finishFragment(); } } @@ -1884,7 +1902,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen delegate.selectedPhotosChanged(); } }); - cell.getCheckFrame().setVisibility(selectPhotoType != 0 ? View.GONE : View.VISIBLE); + cell.getCheckFrame().setVisibility(selectPhotoType != PhotoAlbumPickerActivity.SELECT_TYPE_ALL ? View.GONE : View.VISIBLE); view = cell; break; case 1: @@ -1937,7 +1955,7 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen showing = PhotoViewer.isShowingImage(photoEntry.getPathToAttach()); } imageView.getImageReceiver().setVisible(!showing, true); - cell.getCheckBox().setVisibility(selectPhotoType != 0 || showing ? View.GONE : View.VISIBLE); + cell.getCheckBox().setVisibility(selectPhotoType != PhotoAlbumPickerActivity.SELECT_TYPE_ALL || showing ? View.GONE : View.VISIBLE); break; } case 1: { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java index 81d988c12..c63310af5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java @@ -27,6 +27,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; @@ -38,6 +39,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; +import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.net.Uri; import android.os.Build; @@ -65,6 +67,7 @@ import android.transition.TransitionSet; import android.transition.TransitionValues; import android.util.FloatProperty; import android.util.Property; +import android.util.Range; import android.util.SparseArray; import android.util.TypedValue; import android.view.ActionMode; @@ -86,6 +89,7 @@ import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.OvershootInterpolator; import android.widget.FrameLayout; @@ -114,6 +118,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import org.jetbrains.annotations.NotNull; @@ -171,8 +176,12 @@ import org.telegram.ui.Components.ChatAttachAlert; import org.telegram.ui.Components.CheckBox; import org.telegram.ui.Components.ClippingImageView; import org.telegram.ui.Components.CombinedDrawable; +import org.telegram.ui.Components.Crop.CropTransform; +import org.telegram.ui.Components.Crop.CropView; +import org.telegram.ui.Components.CubicBezierInterpolator; import org.telegram.ui.Components.FadingTextViewLayout; import org.telegram.ui.Components.FilterShaders; +import org.telegram.ui.Components.FloatSeekBarAccessibilityDelegate; import org.telegram.ui.Components.GestureDetector2; import org.telegram.ui.Components.GroupedPhotosListView; import org.telegram.ui.Components.LayoutHelper; @@ -192,14 +201,15 @@ import org.telegram.ui.Components.SizeNotifierFrameLayoutPhoto; import org.telegram.ui.Components.StickersAlert; import org.telegram.ui.Components.TextViewSwitcher; import org.telegram.ui.Components.Tooltip; -import org.telegram.ui.Components.URLSpanNoUnderline; import org.telegram.ui.Components.URLSpanUserMentionPhotoViewer; +import org.telegram.ui.Components.UndoView; import org.telegram.ui.Components.VideoEditTextureView; import org.telegram.ui.Components.VideoForwardDrawable; import org.telegram.ui.Components.VideoPlayer; import org.telegram.ui.Components.VideoPlayerSeekBar; import org.telegram.ui.Components.VideoSeekPreviewImage; import org.telegram.ui.Components.VideoTimelinePlayView; +import org.telegram.ui.Components.ViewHelper; import java.io.ByteArrayInputStream; import java.io.File; @@ -208,6 +218,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Locale; @@ -234,6 +245,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private boolean muteVideo; + private boolean inBubbleMode; + private int slideshowMessageId; private String nameOverride; private int dateOverride; @@ -242,7 +255,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private Runnable miniProgressShowRunnable = () -> toggleMiniProgressInternal(true); private Activity parentActivity; - private Context actvityContext; + private Context activityContext; private ActionBar actionBar; private boolean isActionBarVisible = true; @@ -274,6 +287,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private ActionBarMenuItem sendItem; private ActionBarMenuItem pipItem; private ActionBarMenuItem masksItem; + private LinearLayout itemsLayout; private Map actionBarItemsVisibility = new HashMap<>(3); private ImageView shareButton; private BackgroundDrawable backgroundDrawable = new BackgroundDrawable(0xff000000); @@ -288,6 +302,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private RadialProgressView miniProgressView; private ImageView paintItem; private ImageView cropItem; + private ImageView mirrorItem; private ImageView rotateItem; private ImageView cameraItem; private ImageView tuneItem; @@ -296,6 +311,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private ImageView compressItem; private GroupedPhotosListView groupedPhotosListView; private Tooltip tooltip; + private UndoView hintView; private SelectedPhotosListView selectedPhotosListView; private ListAdapter selectedPhotosAdapter; private AnimatorSet compressItemAnimation; @@ -315,6 +331,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private AnimatorSet currentListViewAnimation; private PhotoCropView photoCropView; + private CropTransform cropTransform = new CropTransform(); private PhotoFilterView photoFilterView; private PhotoPaintView photoPaintView; private AlertDialog visibleDialog; @@ -324,6 +341,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private ChatAttachAlert parentAlert; private PhotoViewerCaptionEnterView captionEditText; private int sendPhotoType; + private boolean cropInitied; private boolean isDocumentsPicker; private boolean needCaptionLayout; private AnimatedFileDrawable currentAnimation; @@ -364,14 +382,30 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (miniProgressView != null && miniProgressView.getVisibility() == View.VISIBLE) { return; } + if (PipInstance == PhotoViewer.this) { + return; + } toggleActionBar(false, true); } } }; private AspectRatioFrameLayout aspectRatioFrameLayout; + private View flashView; + private AnimatorSet flashAnimator; private TextureView videoTextureView; private VideoPlayer videoPlayer; + private boolean manuallyPaused; + private Runnable videoPlayRunnable; + private boolean previousHasTransform; + private float previousCropPx; + private float previousCropPy; + private float previousCropPw; + private float previousCropPh; + private float previousCropScale; + private float previousCropRotation; + private boolean previousCropMirrored; + private int previousCropOrientation; private VideoPlayer injectingVideoPlayer; private SurfaceTexture injectingVideoPlayerSurface; private boolean playerInjected; @@ -392,6 +426,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private VideoPlayerControlFrameLayout videoPlayerControlFrameLayout; private Animator videoPlayerControlAnimator; private boolean videoPlayerControlVisible = true; + private int[] videoPlayerCurrentTime = new int[2]; + private int[] videoPlayerTotalTime = new int[2]; private SimpleTextView videoPlayerTime; private VideoPlayerSeekBar videoPlayerSeekbar; private View videoPlayerSeekbarView; @@ -408,6 +444,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private boolean changingTextureView; private int waitingForFirstTextureUpload; private boolean textureUploaded; + private boolean videoSizeSet; private boolean isInline; private boolean switchingInlineMode; private boolean videoCrossfadeStarted; @@ -428,6 +465,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public final static int SELECT_TYPE_WALLPAPER = 3; public final static int SELECT_TYPE_QR = 10; + public final Property FLASH_VIEW_VALUE = new AnimationProperties.FloatProperty("flashViewAlpha") { + @Override + public void setValue(View object, float value) { + object.setAlpha(value); + if (photoCropView != null) { + photoCropView.setVideoThumbFlashAlpha(value); + } + } + + @Override + public Float get(View object) { + return object.getAlpha(); + } + }; + private static class SavedVideoPosition { public final float position; @@ -439,11 +491,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - private class LinkMovementMethodMy extends LinkMovementMethod { + private class CaptionLinkMovementMethod extends LinkMovementMethod { @Override public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer, @NonNull MotionEvent event) { try { - int action = event.getAction(); boolean result = false; @@ -468,7 +519,23 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (action == MotionEvent.ACTION_UP) { if (link instanceof URLSpan) { String url = ((URLSpan) link).getURL(); - if (parentChatActivity != null && AndroidUtilities.shouldShowUrlInAlert(url)) { + if (url.startsWith("video")) { + if (videoPlayer != null && currentMessageObject != null) { + int seconds = Utilities.parseInt(url); + if (videoPlayer.getDuration() == C.TIME_UNSET) { + seekToProgressPending = seconds / (float) currentMessageObject.getDuration(); + } else { + videoPlayer.seekTo(seconds * 1000L); + } + } + } else if (url.startsWith("#")) { + if (parentActivity instanceof LaunchActivity) { + DialogsActivity fragment = new DialogsActivity(null); + fragment.setSearchString(url); + ((LaunchActivity) parentActivity).presentFragment(fragment, false, true); + closePhoto(false, false); + } + } else if (parentChatActivity != null && AndroidUtilities.shouldShowUrlInAlert(url)) { AlertsCreator.showOpenUrlAlert(parentChatActivity, url, true, true); } else { link.onClick(widget); @@ -476,7 +543,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { link.onClick(widget); } - } else if (action == MotionEvent.ACTION_DOWN) { + } else { // action == MotionEvent.ACTION_DOWN Selection.setSelection(buffer, buffer.getSpanStart(link), buffer.getSpanEnd(link)); } result = true; @@ -484,37 +551,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat Selection.removeSelection(buffer); } } - if (!result) { - result = Touch.onTouchEvent(widget, buffer, event); - } - if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { - AndroidUtilities.runOnUIThread(() -> widget.setOnClickListener(openCaptionEnter)); - - URLSpanNoUnderline[] links = buffer.getSpans(widget.getSelectionStart(), widget.getSelectionEnd(), URLSpanNoUnderline.class); - if (links != null && links.length > 0) { - String url = links[0].getURL(); - if (url.startsWith("video")) { - if (videoPlayer != null && currentMessageObject != null) { - int seconds = Utilities.parseInt(url); - if (videoPlayer.getDuration() == C.TIME_UNSET) { - seekToProgressPending = seconds / (float) currentMessageObject.getDuration(); - } else { - videoPlayer.seekTo(seconds * 1000L); - } - } - } else if (url.startsWith("#")) { - if (parentActivity instanceof LaunchActivity) { - DialogsActivity fragment = new DialogsActivity(null); - fragment.setSearchString(url); - ((LaunchActivity) parentActivity).presentFragment(fragment, false, true); - closePhoto(false, false); - } - } - } - Selection.removeSelection(buffer); - } - return result; + return result || Touch.onTouchEvent(widget, buffer, event); } catch (Exception e) { FileLog.e(e); } @@ -522,6 +560,27 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + private void cancelFlashAnimations() { + if (flashView != null) { + flashView.animate().setListener(null).cancel(); + flashView.setAlpha(0.0f); + } + if (flashAnimator != null) { + flashAnimator.cancel(); + flashAnimator = null; + } + if (photoCropView != null) { + photoCropView.cancelThumbAnimation(); + } + } + + private void cancelVideoPlayRunnable() { + if (videoPlayRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(videoPlayRunnable); + videoPlayRunnable = null; + } + } + private Runnable updateProgressRunnable = new Runnable() { @Override public void run() { @@ -531,26 +590,20 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat float progress = videoPlayer.getCurrentPosition() / (float) videoPlayer.getDuration(); if (!inPreview && (currentEditMode != 0 || videoTimelineView.getVisibility() == View.VISIBLE)) { if (progress >= videoTimelineView.getRightProgress()) { - videoTimelineView.setProgress(0); + videoTimelineView.setProgress(videoTimelineView.getLeftProgress()); videoPlayer.seekTo((int) (videoTimelineView.getLeftProgress() * videoPlayer.getDuration())); - if (muteVideo || currentEditMode != 0) { + manuallyPaused = false; + cancelVideoPlayRunnable(); + if (muteVideo || sendPhotoType == SELECT_TYPE_AVATAR || currentEditMode != 0 || switchingToMode > 0) { videoPlayer.play(); } else { videoPlayer.pause(); } containerView.invalidate(); } else { - progress -= videoTimelineView.getLeftProgress(); - if (progress < 0) { - progress = 0; - } - progress /= (videoTimelineView.getRightProgress() - videoTimelineView.getLeftProgress()); - if (progress > 1) { - progress = 1; - } videoTimelineView.setProgress(progress); } - } else { + } else if (sendPhotoType != SELECT_TYPE_AVATAR) { videoTimelineView.setProgress(progress); } updateVideoPlayerTime(); @@ -571,6 +624,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } if (!inPreview && videoTimelineView.getVisibility() == View.VISIBLE) { if (progress >= videoTimelineView.getRightProgress()) { + manuallyPaused = false; videoPlayer.pause(); videoPlayerSeekbar.setProgress(0); videoPlayer.seekTo((int) (videoTimelineView.getLeftProgress() * videoPlayer.getDuration())); @@ -745,6 +799,28 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } }; + private static class EditState { + public String paintPath; + public String croppedPaintPath; + public MediaController.CropState cropState; + public MediaController.SavedFilterState savedFilterState; + public ArrayList mediaEntities; + public ArrayList croppedMediaEntities; + public long averageDuration; + + public void reset() { + paintPath = null; + cropState = null; + savedFilterState = null; + mediaEntities = null; + croppedPaintPath = null; + croppedMediaEntities = null; + averageDuration = 0; + } + } + + private int currentImageHasFace; + private String currentImageFaceKey; private PaintingOverlay paintingOverlay; private ImageReceiver leftImage = new ImageReceiver(); private ImageReceiver centerImage = new ImageReceiver(); @@ -753,12 +829,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private int switchingToIndex; private MessageObject currentMessageObject; private Uri currentPlayingVideoFile; - private MediaController.SavedFilterState currentSavedFilterState; - private ArrayList currentMediaEntities; - private String currentPaintPath; - private long currentAverageDuration; + private EditState editState = new EditState(); private TLRPC.BotInlineResult currentBotInlineResult; private ImageLocation currentFileLocation; + private ImageLocation currentFileLocationVideo; private SecureDocument currentSecureDocument; private String[] currentFileNames = new String[3]; private PlaceProviderObject currentPlaceObject; @@ -767,9 +841,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private boolean currentVideoFinishedLoading; private ImageReceiver.BitmapHolder currentThumb; private boolean ignoreDidSetImage; + private boolean dontAutoPlay; boolean fromCamera; private int avatarsDialogId; + private boolean canEditAvatar; private boolean isEvent; private int sharedMediaType; private long currentDialogId; @@ -798,6 +874,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private float animationValue; private boolean applying; private long animationStartTime; + private int switchingToMode = -1; private AnimatorSet imageMoveAnimation; private AnimatorSet changeModeAnimation; private GestureDetector2 gestureDetector; @@ -836,11 +913,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private ArrayList imagesArr = new ArrayList<>(); private SparseArray[] imagesByIds = new SparseArray[]{new SparseArray<>(), new SparseArray<>()}; private ArrayList imagesArrLocations = new ArrayList<>(); + private ArrayList imagesArrLocationsVideo = new ArrayList<>(); + private ArrayList imagesArrLocationsSizes = new ArrayList<>(); + private ArrayList imagesArrMessages = new ArrayList<>(); private ArrayList secureDocuments = new ArrayList<>(); private ArrayList avatarsArr = new ArrayList<>(); - private ArrayList imagesArrLocationsSizes = new ArrayList<>(); private ArrayList imagesArrLocals = new ArrayList<>(); - private ImageLocation currentUserAvatarLocation = null; + private ImageLocation currentAvatarLocation = null; private android.graphics.Rect hitRect = new android.graphics.Rect(); @@ -858,7 +937,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private final static int gallery_menu_masks = 13; private final static int gallery_menu_savegif = 14; private final static int gallery_menu_masks2 = 15; - private final static int gallery_menu_setascurrent = 94; + private final static int gallery_menu_set_as_main = 16; + private final static int gallery_menu_edit_avatar = 17; private static DecelerateInterpolator decelerateInterpolator; private static Paint progressPaint; @@ -1285,11 +1365,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } public int getX() { - return (getContainerViewWidth() - (int) (size * scale)) / 2; + return (containerView.getWidth() - (int) (size * scale)) / 2; } public int getY() { - return (getContainerViewHeight() - (int) (size * scale)) / 2; + int y = ((AndroidUtilities.displaySize.y + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0)) - (int) (size * scale)) / 2; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + y -= AndroidUtilities.dp(38); + } + return y; } public void onDraw(Canvas canvas) { @@ -1354,6 +1438,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public ClippingImageView animatingImageView; public int animatingImageViewYOffset; public boolean allowTakeAnimation = true; + public boolean canEdit; } public static class EmptyPhotoViewerProvider implements PhotoViewerProvider { @@ -1461,50 +1546,36 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public boolean canCaptureMorePhotos() { return true; } + + @Override + public void openPhotoForEdit(String file, String thumb, boolean isVideo) { + + } } public interface PhotoViewerProvider { PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index, boolean needPreview); - ImageReceiver.BitmapHolder getThumbForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index); - void willSwitchFromPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index); - void willHidePhotoViewer(); - boolean isPhotoChecked(int index); - int setPhotoChecked(int index, VideoEditedInfo videoEditedInfo); - int setPhotoUnchecked(Object photoEntry); - boolean cancelButtonPressed(); - void needAddMorePhotos(); - void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo, boolean notify, int scheduleDate); - int getSelectedCount(); - void updatePhotoAtIndex(int index); - boolean allowCaption(); - boolean scaleToFill(); - ArrayList getSelectedPhotosOrder(); - HashMap getSelectedPhotos(); - boolean canScrollAway(); - int getPhotoIndex(int index); - void deleteImageAtIndex(int index); - String getDeleteMessageString(); - boolean canCaptureMorePhotos(); + void openPhotoForEdit(String file, String thumb, boolean isVideo); } private class FrameLayoutDrawer extends SizeNotifierFrameLayoutPhoto { @@ -1560,7 +1631,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat continue; } if (child == aspectRatioFrameLayout) { - int heightSpec = MeasureSpec.makeMeasureSpec(AndroidUtilities.displaySize.y + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0), MeasureSpec.EXACTLY); + int heightSpec = MeasureSpec.makeMeasureSpec(AndroidUtilities.displaySize.y + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0), MeasureSpec.EXACTLY); child.measure(widthMeasureSpec, heightSpec); } else if (child == paintingOverlay) { int width; @@ -1578,7 +1649,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } paintingOverlay.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } else if (captionEditText.isPopupView(child)) { - if (AndroidUtilities.isInMultiwindow) { + if (inBubbleMode) { + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize - inputFieldHeight, MeasureSpec.EXACTLY)); + } else if (AndroidUtilities.isInMultiwindow) { if (AndroidUtilities.isTablet()) { child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(Math.min(AndroidUtilities.dp(320), heightSize - inputFieldHeight - AndroidUtilities.statusBarHeight), MeasureSpec.EXACTLY)); } else { @@ -1599,7 +1672,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { captionAbove = false; } - final int topMargin = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); + final int topMargin = (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); final int height = heightSize - topMargin - bottomMargin; ((MarginLayoutParams) captionScrollView.getLayoutParams()).bottomMargin = bottomMargin; child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); @@ -1692,6 +1765,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat childTop = top - AndroidUtilities.dp(sendPhotoType == 4 || sendPhotoType == 5 ? 40 : 15) - child.getMeasuredHeight(); } else if (child == videoTimelineView) { childTop -= pickerView.getHeight(); + if (sendPhotoType == SELECT_TYPE_AVATAR) { + childTop -= AndroidUtilities.dp(52); + } + } else if (child == videoAvatarTooltip) { + childTop -= pickerView.getHeight() + AndroidUtilities.dp(31); } child.layout(childLeft + l, childTop, childLeft + width + l, childTop + height); } @@ -1703,7 +1781,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat protected void onDraw(Canvas canvas) { PhotoViewer.this.onDraw(canvas); - if (Build.VERSION.SDK_INT >= 21 && AndroidUtilities.statusBarHeight != 0 && actionBar != null) { + if (isStatusBarVisible() && AndroidUtilities.statusBarHeight != 0 && actionBar != null) { paint.setAlpha((int) (255 * actionBar.getAlpha() * 0.2f)); canvas.drawRect(0, currentPanTranslationY, getMeasuredWidth(), currentPanTranslationY + AndroidUtilities.statusBarHeight, paint); paint.setAlpha((int) (255 * actionBar.getAlpha() * 0.498f)); @@ -1827,6 +1905,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private float progress = 1f; private boolean seekBarTransitionEnabled; + private boolean translationYAnimationEnabled = true; public VideoPlayerControlFrameLayout(@NonNull Context context) { super(context); @@ -1869,16 +1948,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat float progress = 0; if (videoPlayer != null) { progress = videoPlayer.getCurrentPosition() / (float) videoPlayer.getDuration(); - if (!inPreview && videoTimelineView.getVisibility() == View.VISIBLE) { - progress -= videoTimelineView.getLeftProgress(); - if (progress < 0) { - progress = 0; - } - progress /= (videoTimelineView.getRightProgress() - videoTimelineView.getLeftProgress()); - if (progress > 1) { - progress = 1; - } - } } if (playerWasReady) { videoPlayerSeekbar.setProgress(progress); @@ -1906,7 +1975,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoPlayerTime.setScaleY(1f - 0.1f * (1f - progress)); videoPlayerSeekbar.setTransitionProgress(1f - progress); } else { - setTranslationY(AndroidUtilities.dpf2(24) * (1f - progress)); + if (translationYAnimationEnabled) { + setTranslationY(AndroidUtilities.dpf2(24) * (1f - progress)); + } videoPlayerSeekbarView.setAlpha(progress); } } @@ -1929,6 +2000,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat onProgressChanged(progress); } } + + public void setTranslationYAnimationEnabled(boolean translationYAnimationEnabled) { + if (this.translationYAnimationEnabled != translationYAnimationEnabled) { + this.translationYAnimationEnabled = translationYAnimationEnabled; + if (!translationYAnimationEnabled) { + setTranslationY(0); + } + onProgressChanged(progress); + } + } } private class CaptionTextViewSwitcher extends TextViewSwitcher { @@ -2207,7 +2288,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat @Override public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) { if (dyUnconsumed != 0) { - final int topMargin = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); + final int topMargin = (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); final int dy = Math.round(dyUnconsumed * (1f - Math.abs((-overScrollY / (captionContainer.getTop() - topMargin))))); if (dy != 0) { @@ -2293,10 +2374,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public void invalidate() { super.invalidate(); if (isActionBarVisible) { - final int progressBottom = photoProgressViews[0].getY() + photoProgressViews[0].size; - final int topMargin = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); - final int captionTop = captionContainer.getTop() + (int) captionTextViewSwitcher.getTranslationY() - getScrollY() + topMargin - AndroidUtilities.dp(12); - photoProgressViews[0].setIndexedAlpha(2, captionTop > progressBottom ? 1f : 0f, true); + boolean buttonVisible = getScrollY() == 0; + + if (!buttonVisible) { + final int progressBottom = photoProgressViews[0].getY() + photoProgressViews[0].size; + final int topMargin = (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); + final int captionTop = captionContainer.getTop() + (int) captionTextViewSwitcher.getTranslationY() - getScrollY() + topMargin - AndroidUtilities.dp(12); + buttonVisible = captionTop > progressBottom; + } + + photoProgressViews[0].setIndexedAlpha(2, buttonVisible ? 1f : 0f, true); } } } @@ -2419,8 +2506,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (photos.isEmpty()) { return; } + ArrayList messages = (ArrayList) args[5]; imagesArrLocations.clear(); imagesArrLocationsSizes.clear(); + imagesArrLocationsVideo.clear(); + imagesArrMessages.clear(); avatarsArr.clear(); for (int a = 0; a < photos.size(); a++) { TLRPC.Photo photo = photos.get(a); @@ -2428,11 +2518,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat continue; } TLRPC.PhotoSize sizeFull = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 640); + TLRPC.VideoSize videoSize = photo.video_sizes.isEmpty() ? null : photo.video_sizes.get(0); if (sizeFull != null) { if (setToImage == -1 && currentFileLocation != null) { for (int b = 0; b < photo.sizes.size(); b++) { TLRPC.PhotoSize size = photo.sizes.get(b); - if (size.location.local_id == currentFileLocation.location.local_id && size.location.volume_id == currentFileLocation.location.volume_id) { + if (size.location != null && size.location.local_id == currentFileLocation.location.local_id && size.location.volume_id == currentFileLocation.location.volume_id) { setToImage = imagesArrLocations.size(); break; } @@ -2443,9 +2534,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat sizeFull.location.file_reference = photo.file_reference; } ImageLocation location = ImageLocation.getForPhoto(sizeFull, photo); + ImageLocation videoLocation = videoSize != null ? ImageLocation.getForPhoto(videoSize, photo) : location; if (location != null) { imagesArrLocations.add(location); - imagesArrLocationsSizes.add(sizeFull.size); + imagesArrLocationsSizes.add(videoLocation.currentSize); + imagesArrLocationsVideo.add(videoLocation); + if (messages != null) { + imagesArrMessages.add(messages.get(a)); + } else { + imagesArrMessages.add(null); + } avatarsArr.add(photo); } } @@ -2477,7 +2575,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (location != null) { imagesArrLocations.add(0, location); avatarsArr.add(0, new TLRPC.TL_photoEmpty()); - imagesArrLocationsSizes.add(0, 0); + imagesArrLocationsSizes.add(0, currentFileLocationVideo.currentSize); + imagesArrLocationsVideo.add(0, currentFileLocationVideo); + imagesArrMessages.add(0, null); setImageIndex(0); } } @@ -2590,11 +2690,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (!endReached[loadIndex]) { loadingMoreImages = true; - if (opennedFromMedia) { - MediaDataController.getInstance(currentAccount).loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 40, loadFromMaxId, sharedMediaType, 1, classGuid); - } else { - MediaDataController.getInstance(currentAccount).loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 40, loadFromMaxId, sharedMediaType, 1, classGuid); - } + MediaDataController.getInstance(currentAccount).loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 40, loadFromMaxId, sharedMediaType, 1, classGuid); } } } else { @@ -2636,7 +2732,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (loadInitialVideo) { loadInitialVideo = false; progressView.setVisibility(View.INVISIBLE); - preparePlayer(currentPlayingVideoFile, false, false, currentSavedFilterState); + preparePlayer(currentPlayingVideoFile, false, false, editState.savedFilterState); } else if (tryStartRequestPreviewOnFinish) { releasePlayer(false); tryStartRequestPreviewOnFinish = !MediaController.getInstance().scheduleVideoConvert(videoPreviewMessageObject, true); @@ -2655,7 +2751,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat requestingPreview = false; photoProgressViews[0].setProgress(1f, true); photoProgressViews[0].setBackgroundState(PROGRESS_PLAY, true); - preparePlayer(Uri.fromFile(new File(finalPath)), false, true, currentSavedFilterState); + preparePlayer(Uri.fromFile(new File(finalPath)), false, true, editState.savedFilterState); } } } @@ -2746,8 +2842,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (f == null) { f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); } - } else if (currentFileLocation != null) { - f = FileLoader.getPathToAttach(currentFileLocation.location, avatarsDialogId != 0 || isEvent); + } else if (currentFileLocationVideo != null) { + f = FileLoader.getPathToAttach(getFileLocation(currentFileLocationVideo), getFileLocationExt(currentFileLocationVideo), avatarsDialogId != 0 || isEvent); } if (f.exists()) { @@ -2808,8 +2904,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (parentActivity == activity || activity == null) { return; } + inBubbleMode = activity instanceof BubbleActivity; parentActivity = activity; - actvityContext = new ContextThemeWrapper(parentActivity, R.style.Theme_TMessages); + activityContext = new ContextThemeWrapper(parentActivity, R.style.Theme_TMessages); touchSlop = ViewConfiguration.get(parentActivity).getScaledTouchSlop(); if (progressDrawables == null) { @@ -2844,7 +2941,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat @Override public boolean dispatchKeyEvent(KeyEvent event) { int keyCode = event.getKeyCode(); - if (!muteVideo && isCurrentVideo && videoPlayer != null && event.getRepeatCount() == 0 && event.getAction() == KeyEvent.ACTION_DOWN && (event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP || event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN)) { + if (!muteVideo && sendPhotoType != SELECT_TYPE_AVATAR && isCurrentVideo && videoPlayer != null && event.getRepeatCount() == 0 && event.getAction() == KeyEvent.ACTION_DOWN && (event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_UP || event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN)) { videoPlayer.setVolume(1.0f); } return super.dispatchKeyEvent(event); @@ -2885,17 +2982,19 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (Build.VERSION.SDK_INT >= 21 && lastInsets != null) { WindowInsets insets = (WindowInsets) lastInsets; - if (AndroidUtilities.incorrectDisplaySizeFix) { - if (heightSize > AndroidUtilities.displaySize.y) { - heightSize = AndroidUtilities.displaySize.y; - } - heightSize += AndroidUtilities.statusBarHeight; - } else { - int insetBottom = insets.getStableInsetBottom(); - if (insetBottom >= 0 && AndroidUtilities.statusBarHeight >= 0) { - int newSize = heightSize - AndroidUtilities.statusBarHeight - insets.getStableInsetBottom(); - if (newSize > 0 && newSize < 4096) { - AndroidUtilities.displaySize.y = newSize; + if (!inBubbleMode) { + if (AndroidUtilities.incorrectDisplaySizeFix) { + if (heightSize > AndroidUtilities.displaySize.y) { + heightSize = AndroidUtilities.displaySize.y; + } + heightSize += AndroidUtilities.statusBarHeight; + } else { + int insetBottom = insets.getStableInsetBottom(); + if (insetBottom >= 0 && AndroidUtilities.statusBarHeight >= 0) { + int newSize = heightSize - AndroidUtilities.statusBarHeight - insets.getStableInsetBottom(); + if (newSize > 0 && newSize < 4096) { + AndroidUtilities.displaySize.y = newSize; + } } } } @@ -2914,9 +3013,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat @SuppressWarnings("DrawAllocation") @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - int x = 0; - animatingImageView.layout(x, 0, x + animatingImageView.getMeasuredWidth(), animatingImageView.getMeasuredHeight()); - containerView.layout(x, 0, x + containerView.getMeasuredWidth(), containerView.getMeasuredHeight()); + animatingImageView.layout(0, 0, animatingImageView.getMeasuredWidth(), animatingImageView.getMeasuredHeight()); + containerView.layout(0, 0, containerView.getMeasuredWidth(), containerView.getMeasuredHeight()); wasLayout = true; if (changed) { if (!dontResetZoomOnFirstLayout) { @@ -2931,11 +3029,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat LayoutParams layoutParams = (LayoutParams) checkImageView.getLayoutParams(); WindowManager manager = (WindowManager) ApplicationLoader.applicationContext.getSystemService(Activity.WINDOW_SERVICE); int rotation = manager.getDefaultDisplay().getRotation(); - layoutParams.topMargin = (ActionBar.getCurrentActionBarHeight() - AndroidUtilities.dp(34)) / 2 + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + layoutParams.topMargin = (ActionBar.getCurrentActionBarHeight() - AndroidUtilities.dp(34)) / 2 + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); checkImageView.setLayoutParams(layoutParams); layoutParams = (LayoutParams) photosCounterView.getLayoutParams(); - layoutParams.topMargin = (ActionBar.getCurrentActionBarHeight() - AndroidUtilities.dp(40)) / 2 + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + layoutParams.topMargin = (ActionBar.getCurrentActionBarHeight() - AndroidUtilities.dp(40)) / 2 + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); photosCounterView.setLayoutParams(layoutParams); }); } @@ -3001,8 +3099,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (Build.VERSION.SDK_INT >= 21) { containerView.setFitsSystemWindows(true); containerView.setOnApplyWindowInsetsListener((v, insets) -> { - if (AndroidUtilities.statusBarHeight != insets.getSystemWindowInsetTop()) { - AndroidUtilities.statusBarHeight = insets.getSystemWindowInsetTop(); + int newTopInset = insets.getSystemWindowInsetTop(); + if (parentActivity instanceof LaunchActivity && (newTopInset != 0 || AndroidUtilities.isInMultiwindow) && !inBubbleMode && AndroidUtilities.statusBarHeight != newTopInset) { + AndroidUtilities.statusBarHeight = newTopInset; ((LaunchActivity) parentActivity).drawerLayoutContainer.requestLayout(); } WindowInsets oldInsets = (WindowInsets) lastInsets; @@ -3059,7 +3158,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat actionBar.setTitleColor(0xffffffff); actionBar.setSubtitleColor(0xffffffff); actionBar.setBackgroundColor(Theme.ACTION_BAR_PHOTO_VIEWER_COLOR); - actionBar.setOccupyStatusBar(Build.VERSION.SDK_INT >= 21); + actionBar.setOccupyStatusBar(isStatusBarVisible()); actionBar.setItemsBackgroundColor(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR, false); actionBar.setBackButtonImage(R.drawable.ic_ab_back); actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, 1, 1)); @@ -3088,8 +3187,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); } - } else if (currentFileLocation != null) { - f = FileLoader.getPathToAttach(currentFileLocation.location, avatarsDialogId != 0 || isEvent); + } else if (currentFileLocationVideo != null) { + f = FileLoader.getPathToAttach(getFileLocation(currentFileLocationVideo), getFileLocationExt(currentFileLocationVideo), avatarsDialogId != 0 || isEvent); } if (f != null && f.exists()) { @@ -3109,7 +3208,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat mediaActivity.setChatInfo(parentChatActivity.getCurrentChatInfo()); } closePhoto(false, false); - ((LaunchActivity) parentActivity).presentFragment(mediaActivity, false, true); + if (parentActivity instanceof LaunchActivity) { + ((LaunchActivity) parentActivity).presentFragment(mediaActivity, false, true); + } } } else if (id == gallery_menu_showinchat) { if (currentMessageObject == null) { @@ -3134,13 +3235,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } args.putInt("message_id", currentMessageObject.getId()); NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); - LaunchActivity launchActivity = (LaunchActivity) parentActivity; - boolean remove = launchActivity.getMainFragmentsCount() > 1 || AndroidUtilities.isTablet(); - launchActivity.presentFragment(new ChatActivity(args), remove, true); + if (parentActivity instanceof LaunchActivity) { + LaunchActivity launchActivity = (LaunchActivity) parentActivity; + boolean remove = launchActivity.getMainFragmentsCount() > 1 || AndroidUtilities.isTablet(); + launchActivity.presentFragment(new ChatActivity(args), remove, true); + } currentMessageObject = null; closePhoto(false, false); } else if (id == gallery_menu_send || id == gallery_menu_send_noquote) { - if (currentMessageObject == null || parentActivity == null) { + if (currentMessageObject == null || !(parentActivity instanceof LaunchActivity)) { return; } ((LaunchActivity) parentActivity).switchToAccount(currentMessageObject.currentAccount, true); @@ -3199,20 +3302,39 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (parentActivity == null || placeProvider == null) { return; } + boolean isChannel = false; + if (currentMessageObject != null && !currentMessageObject.scheduled) { + int lower_id = (int) currentMessageObject.getDialogId(); + if (lower_id < 0) { + isChannel = ChatObject.isChannel(MessagesController.getInstance(currentAccount).getChat(-lower_id)); + } + } AlertDialog.Builder builder = new AlertDialog.Builder(parentActivity); String text = placeProvider.getDeleteMessageString(); if (text != null) { builder.setTitle(LocaleController.getString("AreYouSureDeletePhotoTitle", R.string.AreYouSureDeletePhotoTitle)); builder.setMessage(text); - } else if (currentMessageObject != null && currentMessageObject.isVideo()) { + } else if (currentFileLocationVideo != null && currentFileLocationVideo != currentFileLocation || currentMessageObject != null && currentMessageObject.isVideo()) { builder.setTitle(LocaleController.getString("AreYouSureDeleteVideoTitle", R.string.AreYouSureDeleteVideoTitle)); - builder.setMessage(LocaleController.formatString("AreYouSureDeleteVideo", R.string.AreYouSureDeleteVideo)); + if (isChannel) { + builder.setMessage(LocaleController.formatString("AreYouSureDeleteVideoEveryone", R.string.AreYouSureDeleteVideoEveryone)); + } else { + builder.setMessage(LocaleController.formatString("AreYouSureDeleteVideo", R.string.AreYouSureDeleteVideo)); + } } else if (currentMessageObject != null && currentMessageObject.isGif()) { builder.setTitle(LocaleController.getString("AreYouSureDeleteGIFTitle", R.string.AreYouSureDeleteGIFTitle)); - builder.setMessage(LocaleController.formatString("AreYouSureDeleteGIF", R.string.AreYouSureDeleteGIF)); + if (isChannel) { + builder.setMessage(LocaleController.formatString("AreYouSureDeleteGIFEveryone", R.string.AreYouSureDeleteGIFEveryone)); + } else { + builder.setMessage(LocaleController.formatString("AreYouSureDeleteGIF", R.string.AreYouSureDeleteGIF)); + } } else { builder.setTitle(LocaleController.getString("AreYouSureDeletePhotoTitle", R.string.AreYouSureDeletePhotoTitle)); - builder.setMessage(LocaleController.formatString("AreYouSureDeletePhoto", R.string.AreYouSureDeletePhoto)); + if (isChannel) { + builder.setMessage(LocaleController.formatString("AreYouSureDeletePhotoEveryone", R.string.AreYouSureDeletePhotoEveryone)); + } else { + builder.setMessage(LocaleController.formatString("AreYouSureDeletePhoto", R.string.AreYouSureDeletePhoto)); + } } final boolean[] deleteForAll = new boolean[1]; @@ -3240,7 +3362,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } if (currentUser != null && currentUser.id != UserConfig.getInstance(currentAccount).getClientUserId() || currentChat != null) { - if ((currentMessageObject.messageOwner.action == null || currentMessageObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty) && currentMessageObject.isOut() && (currentDate - currentMessageObject.messageOwner.date) <= revokeTimeLimit) { + boolean canRevokeInbox = currentUser != null && MessagesController.getInstance(currentAccount).canRevokePmInbox; + if ((currentMessageObject.messageOwner.action == null || currentMessageObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty) && (currentMessageObject.isOut() || canRevokeInbox || ChatObject.hasAdminRights(currentChat)) && (currentDate - currentMessageObject.messageOwner.date) <= revokeTimeLimit) { FrameLayout frameLayout = new FrameLayout(parentActivity); CheckBoxCell cell = new CheckBoxCell(parentActivity, 1); cell.setBackgroundDrawable(Theme.getSelectorDrawable(false)); @@ -3257,6 +3380,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat cell1.setChecked(deleteForAll[0], true); }); builder.setView(frameLayout); + builder.setCustomViewOffset(9); } } } @@ -3291,28 +3415,25 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (currentIndex < 0 || currentIndex >= avatarsArr.size()) { return; } - TLRPC.Photo photo = avatarsArr.get(currentIndex); - ImageLocation currentLocation = imagesArrLocations.get(currentIndex); - if (photo instanceof TLRPC.TL_photoEmpty) { - photo = null; + TLRPC.Message message = imagesArrMessages.get(currentIndex); + if (message != null) { + ArrayList arr = new ArrayList<>(); + arr.add(message.id); + MessagesController.getInstance(currentAccount).deleteMessages(arr, null, null, MessageObject.getDialogId(message), message.to_id.channel_id, true, false); + NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.reloadDialogPhotos); } - boolean current = false; - if (currentUserAvatarLocation != null) { - if (photo != null) { - for (TLRPC.PhotoSize size : photo.sizes) { - if (size.location.local_id == currentUserAvatarLocation.location.local_id && size.location.volume_id == currentUserAvatarLocation.location.volume_id) { - current = true; - break; - } - } - } else if (currentLocation.location.local_id == currentUserAvatarLocation.location.local_id && currentLocation.location.volume_id == currentUserAvatarLocation.location.volume_id) { - current = true; + if (isCurrentAvatarSet()) { + if (avatarsDialogId > 0) { + MessagesController.getInstance(currentAccount).deleteUserPhoto(null); + } else { + MessagesController.getInstance(currentAccount).changeChatAvatar(-avatarsDialogId, null, null, null, 0, null, null, null); } - } - if (current) { - MessagesController.getInstance(currentAccount).deleteUserPhoto(null); closePhoto(false, false); - } else if (photo != null) { + } else { + TLRPC.Photo photo = avatarsArr.get(currentIndex); + if (photo == null) { + return; + } TLRPC.TL_inputPhoto inputPhoto = new TLRPC.TL_inputPhoto(); inputPhoto.id = photo.id; inputPhoto.access_hash = photo.access_hash; @@ -3320,10 +3441,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (inputPhoto.file_reference == null) { inputPhoto.file_reference = new byte[0]; } - MessagesController.getInstance(currentAccount).deleteUserPhoto(inputPhoto); + if (avatarsDialogId > 0) { + MessagesController.getInstance(currentAccount).deleteUserPhoto(inputPhoto); + } MessagesStorage.getInstance(currentAccount).clearUserPhoto(avatarsDialogId, photo.id); imagesArrLocations.remove(currentIndex); imagesArrLocationsSizes.remove(currentIndex); + imagesArrLocationsVideo.remove(currentIndex); + imagesArrMessages.remove(currentIndex); avatarsArr.remove(currentIndex); if (imagesArrLocations.isEmpty()) { closePhoto(false, false); @@ -3335,6 +3460,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentIndex = -1; setImageIndex(index); } + if (message == null) { + NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.reloadDialogPhotos); + } } } else if (!secureDocuments.isEmpty()) { if (placeProvider == null) { @@ -3399,7 +3527,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (pipItem.getAlpha() != 1.0f) { return; } - switchToPip(); + switchToPip(false); } else if (id == gallery_menu_cancel_loading) { if (currentMessageObject == null) { return; @@ -3419,47 +3547,107 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat MediaDataController.getInstance(currentAccount).addRecentGif(document, (int) (System.currentTimeMillis() / 1000)); } MessagesController.getInstance(currentAccount).saveGif(currentMessageObject, document); - } else if (id == gallery_menu_setascurrent) { - if (!imagesArrLocations.isEmpty()) { - if (currentIndex < 0 || currentIndex >= imagesArrLocations.size()) { + } else if (id == gallery_menu_set_as_main) { + TLRPC.Photo photo = avatarsArr.get(currentIndex); + if (photo == null || photo.sizes.isEmpty()) { + return; + } + TLRPC.PhotoSize bigSize = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 800); + TLRPC.PhotoSize smallSize = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 90); + UserConfig userConfig = UserConfig.getInstance(currentAccount); + if (avatarsDialogId == userConfig.clientUserId) { + TLRPC.TL_photos_updateProfilePhoto req = new TLRPC.TL_photos_updateProfilePhoto(); + req.id = new TLRPC.TL_inputPhoto(); + req.id.id = photo.id; + req.id.access_hash = photo.access_hash; + req.id.file_reference = photo.file_reference; + ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> { + if (response instanceof TLRPC.TL_photos_photo) { + TLRPC.TL_photos_photo photos_photo = (TLRPC.TL_photos_photo) response; + MessagesController.getInstance(currentAccount).putUsers(photos_photo.users, false); + TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(userConfig.clientUserId); + if (photos_photo.photo instanceof TLRPC.TL_photo) { + int idx = avatarsArr.indexOf(photo); + if (idx >= 0) { + avatarsArr.set(idx, photos_photo.photo); + } + if (user != null) { + user.photo.photo_id = photos_photo.photo.id; + userConfig.setCurrentUser(user); + userConfig.saveConfig(true); + } + } + } + })); + + TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(userConfig.clientUserId); + if (user != null) { + user.photo.photo_id = photo.id; + user.photo.dc_id = photo.dc_id; + user.photo.photo_small = smallSize.location; + user.photo.photo_big = bigSize.location; + userConfig.setCurrentUser(user); + userConfig.saveConfig(true); + NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.mainUserInfoChanged); + } + } else { + TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(-avatarsDialogId); + if (chat == null) { return; } - TLRPC.Photo photo = avatarsArr.get(currentIndex); - TLRPC.TL_inputPhoto inputPhoto = new TLRPC.TL_inputPhoto(); - inputPhoto.id = photo.id; - inputPhoto.access_hash = photo.access_hash; - inputPhoto.file_reference = photo.file_reference; - if (inputPhoto.file_reference == null) { - inputPhoto.file_reference = new byte[0]; - } - TLRPC.TL_photos_updateProfilePhoto req = new TLRPC.TL_photos_updateProfilePhoto(); - req.id = inputPhoto; - ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> { - if (error == null) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(UserConfig.getInstance(currentAccount).getClientUserId()); - if (user == null) { - user = UserConfig.getInstance(currentAccount).getCurrentUser(); - if (user == null) { - return; - } - MessagesController.getInstance(currentAccount).putUser(user, false); - } else { - UserConfig.getInstance(currentAccount).setCurrentUser(user); - } - - MessagesStorage.getInstance(currentAccount).clearUserPhotos(user.id); - ArrayList users = new ArrayList<>(); - users.add(user); - MessagesStorage.getInstance(currentAccount).putUsersAndChats(users, null, false, true); - } - AndroidUtilities.runOnUIThread(() -> { - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.updateInterfaces, MessagesController.UPDATE_MASK_ALL); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.mainUserInfoChanged); - UserConfig.getInstance(currentAccount).saveConfig(true); - }); - }); - closePhoto(false, false); + TLRPC.TL_inputChatPhoto inputChatPhoto = new TLRPC.TL_inputChatPhoto(); + inputChatPhoto.id = new TLRPC.TL_inputPhoto(); + inputChatPhoto.id.id = photo.id; + inputChatPhoto.id.access_hash = photo.access_hash; + inputChatPhoto.id.file_reference = photo.file_reference; + MessagesController.getInstance(currentAccount).changeChatAvatar(-avatarsDialogId, inputChatPhoto, null, null, 0, null, null, null); + chat.photo.dc_id = photo.dc_id; + chat.photo.photo_small = smallSize.location; + chat.photo.photo_big = bigSize.location; + NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.updateInterfaces, MessagesController.UPDATE_MASK_AVATAR); } + currentAvatarLocation = ImageLocation.getForPhoto(bigSize, photo); + avatarsArr.remove(currentIndex); + avatarsArr.add(0, photo); + + ImageLocation location = imagesArrLocations.get(currentIndex); + imagesArrLocations.remove(currentIndex); + imagesArrLocations.add(0, location); + + location = imagesArrLocationsVideo.get(currentIndex); + imagesArrLocationsVideo.remove(currentIndex); + imagesArrLocationsVideo.add(0, location); + + Integer size = imagesArrLocationsSizes.get(currentIndex); + imagesArrLocationsSizes.remove(currentIndex); + imagesArrLocationsSizes.add(0, size); + + TLRPC.Message message = imagesArrMessages.get(currentIndex); + imagesArrMessages.remove(currentIndex); + imagesArrMessages.add(0, message); + + currentIndex = -1; + setImageIndex(0); + + groupedPhotosListView.clear(); + groupedPhotosListView.fillList(); + hintView.showWithAction(avatarsDialogId, UndoView.ACTION_PROFILE_PHOTO_CHANGED, currentFileLocationVideo == currentFileLocation ? null : 1); + AndroidUtilities.runOnUIThread(() -> { + if (menuItem == null) { + return; + } + menuItem.hideSubItem(gallery_menu_set_as_main); + }, 300); + } else if (id == gallery_menu_edit_avatar) { + File f = FileLoader.getPathToAttach(getFileLocation(currentFileLocationVideo), getFileLocationExt(currentFileLocationVideo), true); + boolean isVideo = currentFileLocationVideo.imageType == FileLoader.IMAGE_TYPE_ANIMATION; + String thumb; + if (isVideo) { + thumb = FileLoader.getPathToAttach(getFileLocation(currentFileLocation), getFileLocationExt(currentFileLocation), true).getAbsolutePath(); + } else { + thumb = null; + } + placeProvider.openPhotoForEdit(f.getAbsolutePath(), thumb, isVideo); } } @@ -3467,8 +3655,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public boolean canOpenMenu() { if (currentMessageObject != null) { return true; - } else if (currentFileLocation != null) { - File f = FileLoader.getPathToAttach(getFileLocation(currentFileLocation), avatarsDialogId != 0 || isEvent); + } else if (currentFileLocationVideo != null) { + File f = FileLoader.getPathToAttach(getFileLocation(currentFileLocationVideo), getFileLocationExt(currentFileLocationVideo), avatarsDialogId != 0 || isEvent); return f.exists(); } return false; @@ -3478,9 +3666,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat ActionBarMenu menu = actionBar.createMenu(); masksItem = menu.addItem(gallery_menu_masks, R.drawable.deproko_baseline_masks_24); + masksItem.setContentDescription(LocaleController.getString("Masks", R.string.Masks)); pipItem = menu.addItem(gallery_menu_pip, R.drawable.ic_goinline); + pipItem.setContentDescription(LocaleController.getString("AccDescrPipMode", R.string.AccDescrPipMode)); sendNoQuoteItem = menu.addItem(gallery_menu_send_noquote, R.drawable.msg_forward_noquote); + sendNoQuoteItem.setContentDescription(LocaleController.getString("NoQuoteForward", R.string.Forward)); sendItem = menu.addItem(gallery_menu_send, R.drawable.msg_forward); + sendItem.setContentDescription(LocaleController.getString("Forward", R.string.Forward)); menuItem = menu.addItem(0, R.drawable.ic_ab_other); menuItem.addSubItem(gallery_menu_openin, R.drawable.baseline_open_in_browser_24, LocaleController.getString("OpenInExternalApp", R.string.OpenInExternalApp)).setColors(0xfffafafa, 0xfffafafa); @@ -3494,13 +3686,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat menuItem.addSubItem(gallery_menu_save, R.drawable.baseline_image_24, LocaleController.getString("SaveToGallery", R.string.SaveToGallery)).setColors(0xfffafafa, 0xfffafafa); menuItem.addSubItem(gallery_menu_scan, R.drawable.wallet_qr, LocaleController.getString("ScanQRCode", R.string.ScanQRCode)).setColors(0xfffafafa, 0xfffafafa); - menuItem.addSubItem(gallery_menu_setascurrent, R.drawable.baseline_camera_alt_24, LocaleController.getString("SetAsCurrent", R.string.SetAsCurrent)).setColors(0xfffafafa, 0xfffafafa); + menuItem.addSubItem(gallery_menu_set_as_main, R.drawable.baseline_camera_alt_24, LocaleController.getString("SetAsMain", R.string.SetAsMain)).setColors(0xfffafafa, 0xfffafafa); menuItem.addSubItem(gallery_menu_delete, R.drawable.baseline_delete_24, LocaleController.getString("Delete", R.string.Delete)).setColors(0xfffafafa, 0xfffafafa); menuItem.addSubItem(gallery_menu_cancel_loading, R.drawable.baseline_cancel_24, LocaleController.getString("StopDownload", R.string.StopDownload)).setColors(0xfffafafa, 0xfffafafa); menuItem.redrawPopup(0xf9222222); - sendNoQuoteItem.setContentDescription(LocaleController.getString("NoQuoteForward", R.string.NoQuoteForward)); - sendItem.setContentDescription(LocaleController.getString("Forward", R.string.Forward)); menuItem.setSubMenuDelegate(new ActionBarMenuItem.ActionBarSubMenuItemDelegate() { @Override @@ -3518,7 +3708,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } }); - bottomLayout = new FrameLayout(actvityContext); + bottomLayout = new FrameLayout(activityContext); bottomLayout.setBackgroundColor(0x7f000000); containerView.addView(bottomLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.BOTTOM | Gravity.LEFT)); @@ -3527,7 +3717,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat pressedDrawable[1] = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, new int[]{0x32000000, 0}); pressedDrawable[1].setShape(GradientDrawable.RECTANGLE); - groupedPhotosListView = new GroupedPhotosListView(actvityContext, AndroidUtilities.dp(10)); + groupedPhotosListView = new GroupedPhotosListView(activityContext, AndroidUtilities.dp(10)); containerView.addView(groupedPhotosListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 68, Gravity.BOTTOM | Gravity.LEFT, 0, 0, 0, 48)); groupedPhotosListView.setDelegate(new GroupedPhotosListView.GroupedPhotosListViewDelegate() { @Override @@ -3577,12 +3767,29 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentThumb.release(); currentThumb = null; } + dontAutoPlay = true; setImageIndex(index); + dontAutoPlay = false; + } + + @Override + public void onShowAnimationStart() { + containerView.requestLayout(); + } + + @Override + public void onStopScrolling() { + if (shouldMessageObjectAutoPlayed(currentMessageObject)) { + playerAutoStarted = true; + onActionClick(true); + checkProgress(0, false, true); + } } }); + final LinkMovementMethod captionLinkMovementMethod = new CaptionLinkMovementMethod(); captionTextViewSwitcher = new CaptionTextViewSwitcher(containerView.getContext()); - captionTextViewSwitcher.setFactory(this::createCaptionTextView); + captionTextViewSwitcher.setFactory(() -> createCaptionTextView(captionLinkMovementMethod)); captionTextViewSwitcher.setVisibility(View.INVISIBLE); setCaptionHwLayerEnabled(true); @@ -3605,7 +3812,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat photoProgressViews[a].setBackgroundState(PROGRESS_EMPTY, false); } - miniProgressView = new RadialProgressView(actvityContext) { + miniProgressView = new RadialProgressView(activityContext) { @Override public void setAlpha(float alpha) { super.setAlpha(alpha); @@ -3744,7 +3951,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat qualityChooseView.setBackgroundColor(0x7f000000); containerView.addView(qualityChooseView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 70, Gravity.LEFT | Gravity.BOTTOM, 0, 0, 0, 48)); - pickerView = new FrameLayout(actvityContext) { + pickerView = new FrameLayout(activityContext) { @Override public boolean dispatchTouchEvent(MotionEvent ev) { return bottomTouchEnabled && super.dispatchTouchEvent(ev); @@ -3765,6 +3972,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat super.setTranslationY(translationY); if (videoTimelineView != null && videoTimelineView.getVisibility() != GONE) { videoTimelineView.setTranslationY(translationY); + videoAvatarTooltip.setTranslationY(translationY); + } + if (videoAvatarTooltip != null && videoAvatarTooltip.getVisibility() != GONE) { + videoAvatarTooltip.setTranslationY(translationY); } } @@ -3783,6 +3994,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoTimelineView.setVisibility(visibility == VISIBLE ? VISIBLE : INVISIBLE); } } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (itemsLayout.getVisibility() != GONE) { + int x = (right - left - AndroidUtilities.dp(70) - itemsLayout.getMeasuredWidth()) / 2; + itemsLayout.layout(x, itemsLayout.getTop(), x + itemsLayout.getMeasuredWidth(), itemsLayout.getTop() + itemsLayout.getMeasuredHeight()); + } + } }; pickerView.setBackgroundColor(0x7f000000); containerView.addView(pickerView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT)); @@ -3808,18 +4028,25 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoTimelineView = new VideoTimelinePlayView(parentActivity); videoTimelineView.setDelegate(new VideoTimelinePlayView.VideoTimelineViewDelegate() { + + private Runnable seekToRunnable; + private int seekTo; + private boolean wasPlaying; + @Override public void onLeftProgressChanged(float progress) { if (videoPlayer == null) { return; } if (videoPlayer.isPlaying()) { + manuallyPaused = false; videoPlayer.pause(); containerView.invalidate(); } - videoPlayer.seekTo((int) (videoDuration * progress)); + updateAvatarStartTime(1); + seekTo(progress); videoPlayerSeekbar.setProgress(0); - videoTimelineView.setProgress(0); + videoTimelineView.setProgress(progress); updateVideoInfo(); } @@ -3829,12 +4056,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } if (videoPlayer.isPlaying()) { + manuallyPaused = false; videoPlayer.pause(); containerView.invalidate(); } - videoPlayer.seekTo((int) (videoDuration * progress)); + updateAvatarStartTime(2); + seekTo(progress); videoPlayerSeekbar.setProgress(1f); - videoTimelineView.setProgress(1f); + videoTimelineView.setProgress(progress); updateVideoInfo(); } @@ -3843,23 +4072,102 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (videoPlayer == null) { return; } - videoPlayer.seekTo((int) (videoDuration * progress)); + if (sendPhotoType == SELECT_TYPE_AVATAR) { + updateAvatarStartTime(0); + } + seekTo(progress); + } + + private void seekTo(float progress) { + seekTo = (int) (videoDuration * progress); + if (seekToRunnable == null) { + AndroidUtilities.runOnUIThread(seekToRunnable = () -> { + if (videoPlayer != null) { + videoPlayer.seekTo(seekTo); + } + if (sendPhotoType == SELECT_TYPE_AVATAR) { + needCaptureFrameReadyAtTime = seekTo; + if (captureFrameReadyAtTime != needCaptureFrameReadyAtTime) { + captureFrameReadyAtTime = -1; + } + } + seekToRunnable = null; + }, 100); + } + } + + private void updateAvatarStartTime(int fix) { + if (sendPhotoType != SELECT_TYPE_AVATAR) { + return; + } + if (fix != 0) { + if (photoCropView != null && (videoTimelineView.getLeftProgress() > avatarStartProgress || videoTimelineView.getRightProgress() < avatarStartProgress)) { + photoCropView.setVideoThumbVisible(false); + if (fix == 1) { + avatarStartTime = (long) (videoDuration * 1000 * videoTimelineView.getLeftProgress()); + } else { + avatarStartTime = (long) (videoDuration * 1000 * videoTimelineView.getRightProgress()); + } + captureFrameAtTime = -1; + } + } else { + avatarStartProgress = videoTimelineView.getProgress(); + avatarStartTime = (long) (videoDuration * 1000 * avatarStartProgress); + } } @Override - public void didStartDragging() { - + public void didStartDragging(int type) { + if (type == VideoTimelinePlayView.TYPE_PROGRESS) { + cancelVideoPlayRunnable(); + if (sendPhotoType == SELECT_TYPE_AVATAR) { + cancelFlashAnimations(); + captureFrameAtTime = -1; + } + if (wasPlaying = videoPlayer != null && videoPlayer.isPlaying()) { + manuallyPaused = false; + videoPlayer.pause(); + containerView.invalidate(); + } + } } @Override - public void didStopDragging() { - + public void didStopDragging(int type) { + if (seekToRunnable != null) { + AndroidUtilities.cancelRunOnUIThread(seekToRunnable); + seekToRunnable.run(); + } + cancelVideoPlayRunnable(); + if (sendPhotoType == SELECT_TYPE_AVATAR && flashView != null && type == VideoTimelinePlayView.TYPE_PROGRESS) { + cancelFlashAnimations(); + captureFrameAtTime = avatarStartTime; + if (captureFrameReadyAtTime == seekTo) { + captureCurrentFrame(); + } + } else { + if (sendPhotoType == SELECT_TYPE_AVATAR || wasPlaying) { + manuallyPaused = false; + if (videoPlayer != null) { + videoPlayer.play(); + } + } + } } }); videoTimelineView.setVisibility(View.GONE); videoTimelineView.setBackgroundColor(0x7f000000); containerView.addView(videoTimelineView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 58, Gravity.LEFT | Gravity.BOTTOM, 0, 8, 0, 0)); + videoAvatarTooltip = new TextView(parentActivity); + videoAvatarTooltip.setSingleLine(true); + videoAvatarTooltip.setVisibility(View.GONE); + videoAvatarTooltip.setText(LocaleController.getString("ChooseCover", R.string.ChooseCover)); + videoAvatarTooltip.setGravity(Gravity.CENTER_HORIZONTAL); + videoAvatarTooltip.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + videoAvatarTooltip.setTextColor(0xff8c8c8c); + containerView.addView(videoAvatarTooltip, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.BOTTOM, 0, 8, 0, 0)); + pickerViewSendButton = new ImageView(parentActivity) { @Override public boolean dispatchTouchEvent(MotionEvent ev) { @@ -3946,7 +4254,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } int num = a; ActionBarMenuSubItem cell = new ActionBarMenuSubItem(parentActivity); - cell.setBackgroundDrawable(Theme.createSelectorDrawable(0x24ffffff, 7)); if (num == 0) { cell.setTextAndIcon(LocaleController.getString("Translate", R.string.Translate), R.drawable.ic_translate); } else if (num == 1) { @@ -3960,7 +4267,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } cell.setMinimumWidth(AndroidUtilities.dp(196)); cell.setColors(0xffffffff, 0xffffffff); - sendPopupLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 0, 48 * i, 0, 0)); + sendPopupLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 48)); int chatId; if (chat != null) { chatId = chat.id; @@ -3997,6 +4304,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat }); i++; } + sendPopupLayout.setupRadialSelectors(0x24ffffff); sendPopupWindow = new ActionBarPopupWindow(sendPopupLayout, LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT); sendPopupWindow.setAnimationEnabled(false); @@ -4020,19 +4328,78 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return false; }); - LinearLayout itemsLayout = new LinearLayout(parentActivity); + itemsLayout = new LinearLayout(parentActivity) { + + boolean ignoreLayout; + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int visibleItemsCount = 0; + int count = getChildCount(); + for (int a = 0; a < count; a++) { + View v = getChildAt(a); + if (v.getVisibility() != VISIBLE) { + continue; + } + visibleItemsCount++; + } + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + if (visibleItemsCount != 0) { + int itemWidth = Math.min(AndroidUtilities.dp(70), width / visibleItemsCount); + if (compressItem.getVisibility() == VISIBLE) { + ignoreLayout = true; + int compressIconWidth = 64; + if (selectedCompression < 2) { + compressIconWidth = 48; + } else if (selectedCompression == 2) { + compressIconWidth = 64; + } else if (selectedCompression == 3) { + compressIconWidth = 64; + } + int padding = Math.max(0, (itemWidth - AndroidUtilities.dp(compressIconWidth)) / 2); + compressItem.setPadding(padding, 0, padding, 0); + ignoreLayout = false; + } + for (int a = 0; a < count; a++) { + View v = getChildAt(a); + if (v.getVisibility() == GONE) { + continue; + } + v.measure(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + setMeasuredDimension(itemWidth * visibleItemsCount, height); + } else { + setMeasuredDimension(width, height); + } + } + }; itemsLayout.setOrientation(LinearLayout.HORIZONTAL); - pickerView.addView(itemsLayout, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 48, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, 0, 34, 0)); + pickerView.addView(itemsLayout, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 48, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, 0, 70, 0)); cropItem = new ImageView(parentActivity); cropItem.setScaleType(ImageView.ScaleType.CENTER); cropItem.setImageResource(R.drawable.photo_crop); cropItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); - itemsLayout.addView(cropItem, LayoutHelper.createLinear(70, 48)); + itemsLayout.addView(cropItem, LayoutHelper.createLinear(48, 48)); cropItem.setOnClickListener(v -> { if (captionEditText.getTag() != null) { return; } + if (isCurrentVideo) { + if (!videoConvertSupported) { + return; + } + if (videoTextureView instanceof VideoEditTextureView) { + VideoEditTextureView textureView = (VideoEditTextureView) videoTextureView; + if (textureView.getVideoWidth() <= 0 || textureView.getVideoHeight() <= 0) { + return; + } + } else { + return; + } + } switchToEditMode(1); }); cropItem.setContentDescription(LocaleController.getString("CropImage", R.string.CropImage)); @@ -4041,60 +4408,41 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat rotateItem.setScaleType(ImageView.ScaleType.CENTER); rotateItem.setImageResource(R.drawable.tool_rotate); rotateItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); - itemsLayout.addView(rotateItem, LayoutHelper.createLinear(70, 48)); + itemsLayout.addView(rotateItem, LayoutHelper.createLinear(48, 48)); rotateItem.setOnClickListener(v -> { if (photoCropView == null) { return; } - photoCropView.rotate(); + if (photoCropView.rotate()) { + rotateItem.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.MULTIPLY)); + } else { + rotateItem.setColorFilter(null); + } }); rotateItem.setContentDescription(LocaleController.getString("AccDescrRotate", R.string.AccDescrRotate)); - compressItem = new ImageView(parentActivity); - compressItem.setTag(1); - compressItem.setScaleType(ImageView.ScaleType.CENTER); - compressItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - selectedCompression = preferences.getInt("compress_video2", selectCompression()); - int compressIconWidth; - if (selectedCompression <= 1) { - compressItem.setImageResource(R.drawable.video_quality1); - compressIconWidth = 48; - } else if (selectedCompression == 2) { - compressItem.setImageResource(R.drawable.video_quality2); - compressIconWidth = 64; - } else { - selectedCompression = compressionsCount - 1; - compressItem.setImageResource(R.drawable.video_quality3); - compressIconWidth = 64; - } - itemsLayout.addView(compressItem, LayoutHelper.createLinear(70, 48)); - compressItem.setPadding(AndroidUtilities.dp(70 - compressIconWidth) / 2, 0, AndroidUtilities.dp(70 - compressIconWidth) / 2, 0); - compressItem.setOnClickListener(v -> { - if (captionEditText.getTag() != null || muteVideo) { + mirrorItem = new ImageView(parentActivity); + mirrorItem.setScaleType(ImageView.ScaleType.CENTER); + mirrorItem.setImageResource(R.drawable.photo_flip); + mirrorItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); + itemsLayout.addView(mirrorItem, LayoutHelper.createLinear(48, 48)); + mirrorItem.setOnClickListener(v -> { + if (photoCropView == null) { return; } - if (compressItem.getTag() == null) { - if (videoConvertSupported) { - if (tooltip == null) { - tooltip = new Tooltip(activity, containerView, 0xcc111111, Color.WHITE); - } - tooltip.setText(LocaleController.getString("VideoQualityIsTooLow", R.string.VideoQualityIsTooLow)); - tooltip.show(compressItem); - } - return; + if (photoCropView.mirror()) { + mirrorItem.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.MULTIPLY)); + } else { + mirrorItem.setColorFilter(null); } - showQualityView(true); - requestVideoPreview(1); }); - String[] compressionStrings = {"360", "480", "720", "1080"}; - compressItem.setContentDescription(LocaleController.getString("AccDescrVideoQuality", R.string.AccDescrVideoQuality) + ", " + compressionStrings[Math.max(0, selectedCompression)]); + mirrorItem.setContentDescription(LocaleController.getString("AccDescrMirror", R.string.AccDescrMirror)); paintItem = new ImageView(parentActivity); paintItem.setScaleType(ImageView.ScaleType.CENTER); paintItem.setImageResource(R.drawable.photo_paint); paintItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); - itemsLayout.addView(paintItem, LayoutHelper.createLinear(70, 48)); + itemsLayout.addView(paintItem, LayoutHelper.createLinear(48, 48)); paintItem.setOnClickListener(v -> { if (captionEditText.getTag() != null) { return; @@ -4155,7 +4503,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat tuneItem.setScaleType(ImageView.ScaleType.CENTER); tuneItem.setImageResource(R.drawable.photo_tools); tuneItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); - itemsLayout.addView(tuneItem, LayoutHelper.createLinear(70, 48)); + itemsLayout.addView(tuneItem, LayoutHelper.createLinear(48, 48)); tuneItem.setOnClickListener(v -> { if (captionEditText.getTag() != null) { return; @@ -4177,12 +4525,47 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat }); tuneItem.setContentDescription(LocaleController.getString("AccDescrPhotoAdjust", R.string.AccDescrPhotoAdjust)); + compressItem = new ImageView(parentActivity); + compressItem.setTag(1); + compressItem.setScaleType(ImageView.ScaleType.CENTER); + compressItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); + + selectedCompression = selectCompression(); + int compressIconWidth; + if (selectedCompression <= 1) { + compressItem.setImageResource(R.drawable.video_quality1); + } else if (selectedCompression == 2) { + compressItem.setImageResource(R.drawable.video_quality2); + } else { + selectedCompression = compressionsCount - 1; + compressItem.setImageResource(R.drawable.video_quality3); + } + compressItem.setContentDescription(LocaleController.getString("AccDescrVideoQuality", R.string.AccDescrVideoQuality)); + itemsLayout.addView(compressItem, LayoutHelper.createLinear(48, 48)); + compressItem.setOnClickListener(v -> { + if (captionEditText.getTag() != null || muteVideo) { + return; + } + if (compressItem.getTag() == null) { + if (videoConvertSupported) { + if (tooltip == null) { + tooltip = new Tooltip(activity, containerView, 0xcc111111, Color.WHITE); + } + tooltip.setText(LocaleController.getString("VideoQualityIsTooLow", R.string.VideoQualityIsTooLow)); + tooltip.show(compressItem); + } + return; + } + showQualityView(true); + requestVideoPreview(1); + }); + timeItem = new ImageView(parentActivity); timeItem.setScaleType(ImageView.ScaleType.CENTER); timeItem.setImageResource(R.drawable.photo_timer); timeItem.setBackgroundDrawable(Theme.createSelectorDrawable(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR)); timeItem.setContentDescription(LocaleController.getString("SetTimer", R.string.SetTimer)); - itemsLayout.addView(timeItem, LayoutHelper.createLinear(70, 48)); + itemsLayout.addView(timeItem, LayoutHelper.createLinear(48, 48)); timeItem.setOnClickListener(v -> { if (parentActivity == null || captionEditText.getTag() != null) { return; @@ -4325,12 +4708,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat bottomSheet.setBackgroundColor(0xff000000); }); - editorDoneLayout = new PickerBottomLayoutViewer(actvityContext); - editorDoneLayout.setBackgroundColor(0x7f000000); + editorDoneLayout = new PickerBottomLayoutViewer(activityContext); + editorDoneLayout.setBackgroundColor(0xcc000000); editorDoneLayout.updateSelectedCount(0, false); editorDoneLayout.setVisibility(View.GONE); containerView.addView(editorDoneLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM)); - editorDoneLayout.cancelButton.setOnClickListener(view -> switchToEditMode(0)); + editorDoneLayout.cancelButton.setOnClickListener(view -> { + cropTransform.setViewTransform(previousHasTransform, previousCropPx, previousCropPy, previousCropRotation, previousCropOrientation, previousCropScale, 1.0f, 1.0f, previousCropPw, previousCropPh, 0, 0, previousCropMirrored); + switchToEditMode(0); + }); editorDoneLayout.doneButton.setOnClickListener(view -> { if (currentEditMode == 1 && !photoCropView.isReady()) { return; @@ -4339,7 +4725,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat switchToEditMode(0); }); - resetButton = new TextView(actvityContext); + resetButton = new TextView(activityContext); resetButton.setVisibility(View.GONE); resetButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); resetButton.setTextColor(0xffffffff); @@ -4357,17 +4743,18 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat ImageReceiver.ImageReceiverDelegate imageReceiverDelegate = (imageReceiver, set, thumb, memCache) -> { if (imageReceiver == centerImage && set && !thumb) { - if ((currentEditMode == 1 || sendPhotoType == SELECT_TYPE_AVATAR) && photoCropView != null) { + if (!isCurrentVideo && (currentEditMode == 1 || sendPhotoType == SELECT_TYPE_AVATAR) && photoCropView != null) { Bitmap bitmap = imageReceiver.getBitmap(); if (bitmap != null) { - photoCropView.setBitmap(bitmap, imageReceiver.getOrientation(), sendPhotoType != SELECT_TYPE_AVATAR, true, paintingOverlay); + photoCropView.setBitmap(bitmap, imageReceiver.getOrientation(), sendPhotoType != SELECT_TYPE_AVATAR, true, paintingOverlay, cropTransform, null, null); } } if (paintingOverlay.getVisibility() == View.VISIBLE) { containerView.requestLayout(); } + detectFaces(); } - if (imageReceiver == centerImage && set && placeProvider != null && placeProvider.scaleToFill() && !ignoreDidSetImage) { + if (imageReceiver == centerImage && set && placeProvider != null && placeProvider.scaleToFill() && !ignoreDidSetImage && sendPhotoType != SELECT_TYPE_AVATAR) { if (!wasLayout) { dontResetZoomOnFirstLayout = true; } else { @@ -4405,7 +4792,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat checkImageView.setColor(Theme.getColor(Theme.key_dialogFloatingButton), 0xffffffff); checkImageView.setVisibility(View.GONE); containerView.addView(checkImageView, LayoutHelper.createFrame(34, 34, Gravity.RIGHT | Gravity.TOP, 0, rotation == Surface.ROTATION_270 || rotation == Surface.ROTATION_90 ? 61 : 71, 11, 0)); - if (Build.VERSION.SDK_INT >= 21) { + if (isStatusBarVisible()) { ((FrameLayout.LayoutParams) checkImageView.getLayoutParams()).topMargin += AndroidUtilities.statusBarHeight; } checkImageView.setOnClickListener(v -> { @@ -4417,7 +4804,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat photosCounterView = new CounterView(parentActivity); containerView.addView(photosCounterView, LayoutHelper.createFrame(40, 40, Gravity.RIGHT | Gravity.TOP, 0, rotation == Surface.ROTATION_270 || rotation == Surface.ROTATION_90 ? 58 : 68, 64, 0)); - if (Build.VERSION.SDK_INT >= 21) { + if (isStatusBarVisible()) { ((FrameLayout.LayoutParams) photosCounterView.getLayoutParams()).topMargin += AndroidUtilities.statusBarHeight; } photosCounterView.setOnClickListener(v -> { @@ -4461,7 +4848,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat ignoreDidSetImage = false; }); - captionEditText = new PhotoViewerCaptionEnterView(actvityContext, containerView, windowView) { + captionEditText = new PhotoViewerCaptionEnterView(activityContext, containerView, windowView) { @Override public boolean dispatchTouchEvent(MotionEvent ev) { try { @@ -4528,7 +4915,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } containerView.addView(captionEditText, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT)); - mentionListView = new RecyclerListView(actvityContext) { + mentionListView = new RecyclerListView(activityContext) { @Override public boolean dispatchTouchEvent(MotionEvent ev) { return !bottomTouchEnabled && super.dispatchTouchEvent(ev); @@ -4545,7 +4932,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } }; mentionListView.setTag(5); - mentionLayoutManager = new LinearLayoutManager(actvityContext) { + mentionLayoutManager = new LinearLayoutManager(activityContext) { @Override public boolean supportsPredictiveItemAnimations() { return false; @@ -4559,7 +4946,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat mentionListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); containerView.addView(mentionListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 110, Gravity.LEFT | Gravity.BOTTOM)); - mentionListView.setAdapter(mentionsAdapter = new MentionsAdapter(actvityContext, true, 0, new MentionsAdapter.MentionsAdapterDelegate() { + mentionListView.setAdapter(mentionsAdapter = new MentionsAdapter(activityContext, true, 0, new MentionsAdapter.MentionsAdapterDelegate() { @Override public void needChangePanelVisibility(boolean show) { if (show) { @@ -4688,9 +5075,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return false; }); - AccessibilityManager am = (AccessibilityManager) actvityContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + hintView = new UndoView(activityContext); + hintView.setAdditionalTranslationY(AndroidUtilities.dp(112)); + hintView.setColors(0xf9222222, 0xffffffff); + containerView.addView(hintView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT, 8, 0, 8, 8)); + + AccessibilityManager am = (AccessibilityManager) activityContext.getSystemService(Context.ACCESSIBILITY_SERVICE); if (am.isEnabled()) { - playButtonAccessibilityOverlay = new View(actvityContext); + playButtonAccessibilityOverlay = new View(activityContext); playButtonAccessibilityOverlay.setContentDescription(LocaleController.getString("AccActionPlay", R.string.AccActionPlay)); playButtonAccessibilityOverlay.setFocusable(true); containerView.addView(playButtonAccessibilityOverlay, LayoutHelper.createFrame(64, 64, Gravity.CENTER)); @@ -4749,10 +5141,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (captionEditText.getTag() != null) { return; } - if (sendPhotoType == SELECT_TYPE_AVATAR) { - applyCurrentEditMode(); - } if (placeProvider != null && !doneButtonPressed) { + if (sendPhotoType == SELECT_TYPE_AVATAR) { + applyCurrentEditMode(); + } if (parentChatActivity != null) { TLRPC.Chat chat = parentChatActivity.getCurrentChat(); TLRPC.User user = parentChatActivity.getCurrentUser(); @@ -4767,8 +5159,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat ((MediaController.MediaEditState) entry).editedInfo = videoEditedInfo; } } - placeProvider.sendButtonPressed(currentIndex, videoEditedInfo, notify, scheduleDate); doneButtonPressed = true; + placeProvider.sendButtonPressed(currentIndex, videoEditedInfo, notify, scheduleDate); closePhoto(false, false); } } @@ -4795,17 +5187,55 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return false; } - private View.OnClickListener openCaptionEnter = (v) -> { - if (!needCaptionLayout) { + private void captureCurrentFrame() { + if (captureFrameAtTime == -1 || videoTextureView == null) { return; } - openCaptionEnter(); - }; + captureFrameAtTime = -1; + Bitmap bitmap = videoTextureView.getBitmap(); + flashView.animate().alpha(1.0f).setInterpolator(CubicBezierInterpolator.EASE_BOTH).setDuration(85).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + photoCropView.setVideoThumb(bitmap, 0); + flashAnimator = new AnimatorSet(); + flashAnimator.playTogether(ObjectAnimator.ofFloat(flashView, FLASH_VIEW_VALUE, 0.0f)); + flashAnimator.setDuration(85); + flashAnimator.setInterpolator(CubicBezierInterpolator.EASE_OUT); + flashAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (flashAnimator == null) { + return; + } + AndroidUtilities.runOnUIThread(videoPlayRunnable = () -> { + manuallyPaused = false; + if (videoPlayer != null) { + videoPlayer.play(); + } + videoPlayRunnable = null; + }, 860); + } + + @Override + public void onAnimationCancel(Animator animation) { + flashAnimator = null; + } + }); + flashAnimator.start(); + } + }).start(); + } + + private TextView createCaptionTextView(LinkMovementMethod linkMovementMethod) { + TextView textView = new TextView(activityContext) { + + private boolean handleClicks; - private TextView createCaptionTextView() { - TextView textView = new TextView(actvityContext) { @Override public boolean onTouchEvent(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + handleClicks = needCaptionLayout; + } boolean b = super.onTouchEvent(event); return bottomTouchEnabled && b; } @@ -4814,18 +5244,32 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public void scrollTo(int x, int y) { if (getParent().getParent() == pickerView) { super.scrollTo(x, y); - setOnClickListener(null); + handleClicks = false; + } + } + + @Override + public boolean performClick() { + return handleClicks && super.performClick(); + } + + @Override + public void setPressed(boolean pressed) { + final boolean needsRefresh = pressed != isPressed(); + super.setPressed(pressed); + if (needsRefresh) { + invalidate(); } } }; - textView.setMovementMethod(new LinkMovementMethodMy()); - textView.setPadding(AndroidUtilities.dp(16), AndroidUtilities.dp(8), AndroidUtilities.dp(16), AndroidUtilities.dp(8)); + textView.setMovementMethod(linkMovementMethod); + ViewHelper.setPadding(textView, 16, 8, 16, 8); textView.setLinkTextColor(0xff76c2f1); textView.setTextColor(0xffffffff); textView.setHighlightColor(0x33ffffff); - textView.setGravity(Gravity.CENTER_VERTICAL | (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT)); + textView.setGravity(Gravity.CENTER_VERTICAL | LayoutHelper.getAbsoluteGravityStart()); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); - textView.setOnClickListener(openCaptionEnter); + textView.setOnClickListener((v) -> openCaptionEnter()); return textView; } @@ -4846,7 +5290,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private void dismissInternal() { try { if (windowView.getParent() != null) { - ((LaunchActivity) parentActivity).drawerLayoutContainer.setAllowDrawContent(true); + if (parentActivity instanceof LaunchActivity) { + ((LaunchActivity) parentActivity).drawerLayoutContainer.setAllowDrawContent(true); + } WindowManager wm = (WindowManager) parentActivity.getSystemService(Context.WINDOW_SERVICE); wm.removeView(windowView); } @@ -4855,7 +5301,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - private void switchToPip() { + private void switchToPip(boolean fromGesture) { if (videoPlayer == null || !textureUploaded || !checkInlinePermissions() || changingTextureView || switchingInlineMode || isInline) { return; } @@ -4867,7 +5313,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat Instance = null; switchingInlineMode = true; isVisible = false; - if (currentPlaceObject != null) { + AndroidUtilities.cancelRunOnUIThread(hideActionBarRunnable); + if (currentPlaceObject != null && !currentPlaceObject.imageReceiver.getVisible()) { currentPlaceObject.imageReceiver.setVisible(true, true); AnimatedFileDrawable animation = currentPlaceObject.imageReceiver.getAnimation(); if (animation != null) { @@ -4883,6 +5330,20 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } animation.seekTo(videoPlayer.getCurrentPosition(), true); + if (fromGesture) { + currentPlaceObject.imageReceiver.setAlpha(0); + ImageReceiver imageReceiver = currentPlaceObject.imageReceiver; + ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1f); + valueAnimator.addUpdateListener((a) -> imageReceiver.setAlpha((Float) a.getAnimatedValue())); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + imageReceiver.setAlpha(1f); + } + }); + valueAnimator.setDuration(250); + valueAnimator.start(); + } currentPlaceObject.imageReceiver.setAllowStartAnimation(true); currentPlaceObject.imageReceiver.startAnimation(); } @@ -4892,26 +5353,59 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat org.telegram.ui.Components.Rect rect = PipVideoView.getPipRect(aspectRatioFrameLayout.getAspectRatio()); float scale = rect.width / videoTextureView.getWidth(); - rect.y += AndroidUtilities.statusBarHeight; + + ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1f); + float fromX = videoTextureView.getTranslationX(); + float fromY = videoTextureView.getTranslationY() + translationY; + float fromY2 = textureImageView.getTranslationY() + translationY; + float toX = rect.x; + float toX2 = rect.x - aspectRatioFrameLayout.getX() + getLeftInset(); + + float toY = rect.y; + float toY2 = rect.y - aspectRatioFrameLayout.getY(); + + textureImageView.setTranslationY(fromY2); + videoTextureView.setTranslationY(fromY); + + translationY = 0; + containerView.invalidate(); + + CubicBezierInterpolator interpolator; + if (fromGesture) { + if (fromY < toY2) { + interpolator = new CubicBezierInterpolator(0.5, 0, 0.9, 0.9); + } else { + interpolator = new CubicBezierInterpolator(0, 0.5, 0.9, 0.9); + } + } else { + interpolator = null; + } + valueAnimator.addUpdateListener(animation -> { + float xValue = (float) animation.getAnimatedValue(); + float yValue = interpolator == null ? xValue : interpolator.getInterpolation(xValue); + textureImageView.setTranslationX(fromX * (1f - xValue) + toX * xValue); + textureImageView.setTranslationY(fromY2 * (1f - yValue) + toY * yValue); + + videoTextureView.setTranslationX(fromX * (1f - xValue) + (toX2) * xValue); + videoTextureView.setTranslationY(fromY * (1f - yValue) + (toY2) * yValue); + }); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( ObjectAnimator.ofFloat(textureImageView, View.SCALE_X, scale), ObjectAnimator.ofFloat(textureImageView, View.SCALE_Y, scale), - ObjectAnimator.ofFloat(textureImageView, View.TRANSLATION_X, rect.x), - ObjectAnimator.ofFloat(textureImageView, View.TRANSLATION_Y, rect.y), ObjectAnimator.ofFloat(videoTextureView, View.SCALE_X, scale), ObjectAnimator.ofFloat(videoTextureView, View.SCALE_Y, scale), - ObjectAnimator.ofFloat(videoTextureView, View.TRANSLATION_X, rect.x - aspectRatioFrameLayout.getX() + getLeftInset()), - ObjectAnimator.ofFloat(videoTextureView, View.TRANSLATION_Y, rect.y - aspectRatioFrameLayout.getY()), ObjectAnimator.ofInt(backgroundDrawable, AnimationProperties.COLOR_DRAWABLE_ALPHA, 0), - ObjectAnimator.ofFloat(actionBar, View.ALPHA, 0.0f), - ObjectAnimator.ofFloat(bottomLayout, View.ALPHA, 0.0f), - ObjectAnimator.ofFloat(captionTextViewSwitcher, View.ALPHA, 0.0f), - ObjectAnimator.ofFloat(groupedPhotosListView, View.ALPHA, 0.0f) + valueAnimator ); - animatorSet.setInterpolator(new DecelerateInterpolator()); - animatorSet.setDuration(250); + if (fromGesture) { + animatorSet.setInterpolator(CubicBezierInterpolator.EASE_OUT); + animatorSet.setDuration(300); + } else { + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.setDuration(250); + } animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -4920,6 +5414,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } }); animatorSet.start(); + if (!fromGesture) { + toggleActionBar(false, true, new ActionBarToggleParams().enableStatusBarAnimation(false).animationDuration(250).animationInterpolator(new DecelerateInterpolator())); + } } else { switchToInlineRunnable.run(); dismissInternal(); @@ -4964,7 +5461,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat org.telegram.ui.Components.Rect rect = PipVideoView.getPipRect(aspectRatioFrameLayout.getAspectRatio()); float scale = rect.width / textureImageView.getLayoutParams().width; - rect.y += AndroidUtilities.statusBarHeight; textureImageView.setScaleX(scale); textureImageView.setScaleY(scale); textureImageView.setTranslationX(rect.x); @@ -5034,18 +5530,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoPlayerControlFrameLayout = new VideoPlayerControlFrameLayout(containerView.getContext()); containerView.addView(videoPlayerControlFrameLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.BOTTOM | Gravity.LEFT)); - videoPlayerSeekbarView = new View(containerView.getContext()) { - @Override - protected void onDraw(Canvas canvas) { - videoPlayerSeekbar.draw(canvas); - } - }; - videoPlayerControlFrameLayout.addView(videoPlayerSeekbarView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); - - videoPlayerSeekbar = new VideoPlayerSeekBar(videoPlayerSeekbarView); - videoPlayerSeekbar.setHorizontalPadding(AndroidUtilities.dp(2)); - videoPlayerSeekbar.setColors(0x33ffffff, 0x33ffffff, Color.WHITE, Color.WHITE, Color.WHITE, 0x59ffffff); - videoPlayerSeekbar.setDelegate(new VideoPlayerSeekBar.SeekBarDelegate() { + final VideoPlayerSeekBar.SeekBarDelegate seekBarDelegate = new VideoPlayerSeekBar.SeekBarDelegate() { @Override public void onSeekBarDrag(float progress) { if (videoPlayer != null) { @@ -5071,7 +5556,42 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat showVideoSeekPreviewPosition(true); updateVideoSeekPreviewPosition(); } - }); + }; + + final FloatSeekBarAccessibilityDelegate accessibilityDelegate = new FloatSeekBarAccessibilityDelegate() { + @Override + public float getProgress() { + return videoPlayerSeekbar.getProgress(); + } + + @Override + public void setProgress(float progress) { + seekBarDelegate.onSeekBarDrag(progress); + videoPlayerSeekbar.setProgress(progress); + videoPlayerSeekbarView.invalidate(); + } + + @Override + public String getContentDescription(View host) { + final String time = LocaleController.formatPluralString("Minutes", videoPlayerCurrentTime[0]) + ' ' + LocaleController.formatPluralString("Seconds", videoPlayerCurrentTime[1]); + final String totalTime = LocaleController.formatPluralString("Minutes", videoPlayerTotalTime[0]) + ' ' + LocaleController.formatPluralString("Seconds", videoPlayerTotalTime[1]); + return LocaleController.formatString("AccDescrPlayerDuration", R.string.AccDescrPlayerDuration, time, totalTime); + } + }; + videoPlayerSeekbarView = new View(containerView.getContext()) { + @Override + protected void onDraw(Canvas canvas) { + videoPlayerSeekbar.draw(canvas); + } + }; + videoPlayerSeekbarView.setAccessibilityDelegate(accessibilityDelegate); + videoPlayerSeekbarView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + videoPlayerControlFrameLayout.addView(videoPlayerSeekbarView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + videoPlayerSeekbar = new VideoPlayerSeekBar(videoPlayerSeekbarView); + videoPlayerSeekbar.setHorizontalPadding(AndroidUtilities.dp(2)); + videoPlayerSeekbar.setColors(0x33ffffff, 0x33ffffff, Color.WHITE, Color.WHITE, Color.WHITE, 0x59ffffff); + videoPlayerSeekbar.setDelegate(seekBarDelegate); videoPreviewFrame = new VideoSeekPreviewImage(containerView.getContext(), () -> { if (needShowOnReady) { @@ -5099,6 +5619,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoPlayerTime.setTextColor(0xffffffff); videoPlayerTime.setGravity(Gravity.RIGHT | Gravity.TOP); videoPlayerTime.setTextSize(14); + videoPlayerTime.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); videoPlayerControlFrameLayout.addView(videoPlayerTime, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.RIGHT | Gravity.TOP, 0, 15, 12, 0)); } @@ -5124,11 +5645,39 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + private int[] fixVideoWidthHeight(int w, int h) { + int[] result = new int[]{w, h}; + if (Build.VERSION.SDK_INT >= 21) { + MediaCodec encoder = null; + try { + encoder = MediaCodec.createEncoderByType(MediaController.VIDEO_MIME_TYPE); + MediaCodecInfo.CodecCapabilities capabilities = encoder.getCodecInfo().getCapabilitiesForType(MediaController.VIDEO_MIME_TYPE); + MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities(); + Range widths = videoCapabilities.getSupportedWidths(); + Range heights = videoCapabilities.getSupportedHeights(); + result[0] = Math.max(widths.getLower(), Math.round(w / 16.0f) * 16); + result[1] = Math.max(heights.getLower(), Math.round(h / 16.0f) * 16); + } catch (Exception ignore) { + + } finally { + try { + if (encoder != null) { + encoder.release(); + } + } catch (Exception ignore) { + + } + } + } + return result; + } + private VideoEditedInfo getCurrentVideoEditedInfo() { if (!isCurrentVideo && hasAnimatedMediaEntities() && centerImage.getBitmapWidth() > 0) { + float maxSize = sendPhotoType == SELECT_TYPE_AVATAR ? 800 : 854; VideoEditedInfo videoEditedInfo = new VideoEditedInfo(); videoEditedInfo.start = videoEditedInfo.startTime = 0; - videoEditedInfo.endTime = Math.min(3000, currentAverageDuration); + videoEditedInfo.endTime = Math.min(3000, editState.averageDuration); while (videoEditedInfo.endTime > 0 && videoEditedInfo.endTime < 1000) { videoEditedInfo.endTime *= 2; } @@ -5139,28 +5688,46 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoEditedInfo.estimatedDuration = videoEditedInfo.endTime; videoEditedInfo.framerate = 30; videoEditedInfo.originalDuration = videoEditedInfo.endTime; - videoEditedInfo.filterState = currentSavedFilterState; - videoEditedInfo.paintPath = currentPaintPath; - videoEditedInfo.mediaEntities = currentMediaEntities; + videoEditedInfo.filterState = editState.savedFilterState; + if (editState.croppedPaintPath != null) { + videoEditedInfo.paintPath = editState.croppedPaintPath; + videoEditedInfo.mediaEntities = editState.croppedMediaEntities != null && !editState.croppedMediaEntities.isEmpty() ? editState.croppedMediaEntities : null; + } else { + videoEditedInfo.paintPath = editState.paintPath; + videoEditedInfo.mediaEntities = editState.mediaEntities; + } videoEditedInfo.isPhoto = true; int width = centerImage.getBitmapWidth(); int height = centerImage.getBitmapHeight(); - float scale = Math.max(width / 854.0f, height / 854.0f); + if (editState.cropState != null) { + if (editState.cropState.transformRotation == 90 || editState.cropState.transformRotation == 270) { + int temp = width; + width = height; + height = temp; + } + width *= editState.cropState.cropPw; + height *= editState.cropState.cropPh; + } + if (sendPhotoType == SELECT_TYPE_AVATAR) { + width = height; + } + float scale = Math.max(width / maxSize, height / maxSize); if (scale < 1) { scale = 1; } width /= scale; height /= scale; if (width % 16 != 0) { - width = Math.round(width / 16.0f) * 16; + width = Math.max(1, Math.round(width / 16.0f)) * 16; } if (height % 16 != 0) { - height = Math.round(height / 16.0f) * 16; + height = Math.max(1, Math.round(height / 16.0f)) * 16; } videoEditedInfo.originalWidth = videoEditedInfo.resultWidth = width; videoEditedInfo.originalHeight = videoEditedInfo.resultHeight = height; videoEditedInfo.bitrate = -1; videoEditedInfo.muted = true; + videoEditedInfo.avatarStartTime = 0; return videoEditedInfo; } if (!isCurrentVideo || currentPlayingVideoFile == null || compressionsCount == 0) { @@ -5180,25 +5747,65 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoEditedInfo.estimatedDuration = estimatedDuration; videoEditedInfo.framerate = videoFramerate; videoEditedInfo.originalDuration = (long) (videoDuration * 1000); - videoEditedInfo.filterState = currentSavedFilterState; - videoEditedInfo.paintPath = currentPaintPath; - videoEditedInfo.mediaEntities = currentMediaEntities != null && !currentMediaEntities.isEmpty() ? currentMediaEntities : null; + videoEditedInfo.filterState = editState.savedFilterState; + if (editState.croppedPaintPath != null) { + videoEditedInfo.paintPath = editState.croppedPaintPath; + videoEditedInfo.mediaEntities = editState.croppedMediaEntities != null && !editState.croppedMediaEntities.isEmpty() ? editState.croppedMediaEntities : null; + } else { + videoEditedInfo.paintPath = editState.paintPath; + videoEditedInfo.mediaEntities = editState.mediaEntities != null && !editState.mediaEntities.isEmpty() ? editState.mediaEntities : null; + } - if (!muteVideo && (compressItem.getTag() == null || (videoEditedInfo.resultWidth == originalWidth && videoEditedInfo.resultHeight == originalHeight))) { + if (sendPhotoType != SELECT_TYPE_AVATAR && !muteVideo && (compressItem.getTag() == null || (videoEditedInfo.resultWidth == originalWidth && videoEditedInfo.resultHeight == originalHeight))) { videoEditedInfo.resultWidth = originalWidth; videoEditedInfo.resultHeight = originalHeight; videoEditedInfo.bitrate = muteVideo ? -1 : originalBitrate; - videoEditedInfo.muted = muteVideo; } else { - if (muteVideo) { + if (muteVideo || sendPhotoType == SELECT_TYPE_AVATAR) { selectedCompression = 1; updateWidthHeightBitrateForCompression(); } videoEditedInfo.resultWidth = resultWidth; videoEditedInfo.resultHeight = resultHeight; - videoEditedInfo.bitrate = muteVideo ? -1 : bitrate; - videoEditedInfo.muted = muteVideo; + videoEditedInfo.bitrate = muteVideo || sendPhotoType == SELECT_TYPE_AVATAR ? -1 : bitrate; } + videoEditedInfo.cropState = editState.cropState; + if (videoEditedInfo.cropState != null) { + videoEditedInfo.rotationValue += videoEditedInfo.cropState.transformRotation; + while (videoEditedInfo.rotationValue >= 360) { + videoEditedInfo.rotationValue -= 360; + } + if (videoEditedInfo.rotationValue == 90 || videoEditedInfo.rotationValue == 270) { + videoEditedInfo.cropState.transformWidth = (int) (videoEditedInfo.resultWidth * videoEditedInfo.cropState.cropPh); + videoEditedInfo.cropState.transformHeight = (int) (videoEditedInfo.resultHeight * videoEditedInfo.cropState.cropPw); + } else { + videoEditedInfo.cropState.transformWidth = (int) (videoEditedInfo.resultWidth * videoEditedInfo.cropState.cropPw); + videoEditedInfo.cropState.transformHeight = (int) (videoEditedInfo.resultHeight * videoEditedInfo.cropState.cropPh); + } + if (sendPhotoType == SELECT_TYPE_AVATAR) { + if (videoEditedInfo.cropState.transformWidth > 800) { + videoEditedInfo.cropState.transformWidth = 800; + } + if (videoEditedInfo.cropState.transformHeight > 800) { + videoEditedInfo.cropState.transformHeight = 800; + } + videoEditedInfo.cropState.transformWidth = videoEditedInfo.cropState.transformHeight = Math.min(videoEditedInfo.cropState.transformWidth, videoEditedInfo.cropState.transformHeight); + } + if (BuildVars.LOGS_ENABLED) { + FileLog.d("original transformed w = " + videoEditedInfo.cropState.transformWidth + " h = " + videoEditedInfo.cropState.transformHeight + " r = " + videoEditedInfo.rotationValue); + } + int[] fixedSize = fixVideoWidthHeight(videoEditedInfo.cropState.transformWidth, videoEditedInfo.cropState.transformHeight); + videoEditedInfo.cropState.transformWidth = fixedSize[0]; + videoEditedInfo.cropState.transformHeight = fixedSize[1]; + if (BuildVars.LOGS_ENABLED) { + FileLog.d("fixed transformed w = " + videoEditedInfo.cropState.transformWidth + " h = " + videoEditedInfo.cropState.transformHeight); + } + } + if (sendPhotoType == SELECT_TYPE_AVATAR) { + videoEditedInfo.avatarStartTime = avatarStartTime; + videoEditedInfo.originalBitrate = originalBitrate; + } + videoEditedInfo.muted = muteVideo || sendPhotoType == SELECT_TYPE_AVATAR; return videoEditedInfo; } @@ -5250,34 +5857,26 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } private void updateVideoPlayerTime() { - String newText; - if (videoPlayer == null) { - newText = String.format("%02d:%02d / %02d:%02d", 0, 0, 0, 0); - } else { - long current = videoPlayer.getCurrentPosition(); - if (current < 0) { - current = 0; - } - long total = videoPlayer.getDuration(); - if (total < 0) { - total = 0; - } - if (total != C.TIME_UNSET && current != C.TIME_UNSET) { - if (!inPreview && videoTimelineView.getVisibility() == View.VISIBLE) { - total *= (videoTimelineView.getRightProgress() - videoTimelineView.getLeftProgress()); - current -= videoTimelineView.getLeftProgress() * total; - if (current > total) { - current = total; - } + Arrays.fill(videoPlayerCurrentTime, 0); + Arrays.fill(videoPlayerTotalTime, 0); + if (videoPlayer != null) { + long current = Math.max(0, videoPlayer.getCurrentPosition()); + long total = Math.max(0, videoPlayer.getDuration()); + if (!inPreview && videoTimelineView.getVisibility() == View.VISIBLE) { + total *= (videoTimelineView.getRightProgress() - videoTimelineView.getLeftProgress()); + current -= videoTimelineView.getLeftProgress() * total; + if (current > total) { + current = total; } - current /= 1000; - total /= 1000; - newText = String.format("%02d:%02d / %02d:%02d", current / 60, current % 60, total / 60, total % 60); - } else { - newText = String.format("%02d:%02d / %02d:%02d", 0, 0, 0, 0); } + current /= 1000; + total /= 1000; + videoPlayerCurrentTime[0] = (int) (current / 60); + videoPlayerCurrentTime[1] = (int) (current % 60); + videoPlayerTotalTime[0] = (int) (total / 60); + videoPlayerTotalTime[1] = (int) (total % 60); } - videoPlayerTime.setText(newText); + videoPlayerTime.setText(String.format(Locale.ROOT, "%02d:%02d / %02d:%02d", videoPlayerCurrentTime[0], videoPlayerCurrentTime[1], videoPlayerTotalTime[0], videoPlayerTotalTime[1])); } private void checkBufferedProgress(float progress) { @@ -5326,6 +5925,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (tuneItem != null && tuneItem.getColorFilter() != null) { tuneItem.setColorFilter(filter); } + if (rotateItem != null && rotateItem.getColorFilter() != null) { + rotateItem.setColorFilter(filter); + } + if (mirrorItem != null && mirrorItem.getColorFilter() != null) { + mirrorItem.setColorFilter(filter); + } if (editorDoneLayout != null) { editorDoneLayout.doneButton.setTextColor(color); } @@ -5341,6 +5946,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (captionEditText != null) { captionEditText.updateColors(); } + if (videoTimelineView != null) { + videoTimelineView.invalidate(); + } if (selectedPhotosListView != null) { int count = selectedPhotosListView.getChildCount(); for (int a = 0; a < count; a++) { @@ -5380,7 +5988,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private boolean isAccessibilityEnabled() { try { - AccessibilityManager am = (AccessibilityManager) actvityContext.getSystemService(Context.ACCESSIBILITY_SERVICE); + AccessibilityManager am = (AccessibilityManager) activityContext.getSystemService(Context.ACCESSIBILITY_SERVICE); return am.isEnabled(); } catch (Exception e) { FileLog.e(e); @@ -5439,7 +6047,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (aspectRatioFrameLayout.getVisibility() != View.VISIBLE) { aspectRatioFrameLayout.setVisibility(View.VISIBLE); } - if (!pipItem.isEnabled()) { + if (!pipItem.isEnabled() && pipItem.getVisibility() == View.VISIBLE) { pipAvailable = true; pipItem.setEnabled(true); pipItem.animate().alpha(1.0f).setDuration(175).withEndAction(null).start(); @@ -5462,11 +6070,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (!isPlaying) { isPlaying = true; photoProgressViews[0].setBackgroundState(isCurrentVideo ? PROGRESS_NONE : PROGRESS_PAUSE, false); - photoProgressViews[0].setIndexedAlpha(1, !isCurrentVideo && !isAccessibilityEnabled() && ((playerAutoStarted && !playerWasPlaying) || !isActionBarVisible) ? 0f : 1f, false); + photoProgressViews[0].setIndexedAlpha(1, !isCurrentVideo && (!isAccessibilityEnabled() || playerWasPlaying) && ((playerAutoStarted && !playerWasPlaying) || !isActionBarVisible) ? 0f : 1f, false); playerWasPlaying = true; AndroidUtilities.runOnUIThread(updateProgressRunnable); } - } else if (isPlaying) { + } else if (isPlaying || playbackState == ExoPlayer.STATE_ENDED) { isPlaying = false; if (currentEditMode != 3) { photoProgressViews[0].setIndexedAlpha(1, 1f, false); @@ -5476,13 +6084,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (playbackState == ExoPlayer.STATE_ENDED) { if (isCurrentVideo) { if (!videoTimelineView.isDragging()) { - videoTimelineView.setProgress(0.0f); + videoTimelineView.setProgress(videoTimelineView.getLeftProgress()); if (!inPreview && (currentEditMode != 0 || videoTimelineView.getVisibility() == View.VISIBLE)) { videoPlayer.seekTo((int) (videoTimelineView.getLeftProgress() * videoPlayer.getDuration())); } else { videoPlayer.seekTo(0); } - if (currentEditMode == 0) { + manuallyPaused = false; + cancelVideoPlayRunnable(); + if (sendPhotoType != SELECT_TYPE_AVATAR && currentEditMode == 0 && switchingToMode <= 0) { videoPlayer.pause(); } else { videoPlayer.play(); @@ -5497,6 +6107,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { videoPlayer.seekTo(0); } + manuallyPaused = false; videoPlayer.pause(); if (!isActionBarVisible) { toggleActionBar(true, true); @@ -5520,7 +6131,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private void preparePlayer(Uri uri, boolean playWhenReady, boolean preview, MediaController.SavedFilterState savedFilterState) { if (!preview) { currentPlayingVideoFile = uri; - currentSavedFilterState = savedFilterState; } if (parentActivity == null) { return; @@ -5544,10 +6154,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat containerView.addView(textureImageView); } textureUploaded = false; + videoSizeSet = false; videoCrossfadeStarted = false; boolean newPlayerCreated = false; playerWasReady = false; playerWasPlaying = false; + captureFrameReadyAtTime = -1; + captureFrameAtTime = -1; + needCaptureFrameReadyAtTime = -1; if (videoPlayer == null) { if (injectingVideoPlayer != null) { videoPlayer = injectingVideoPlayer; @@ -5569,6 +6183,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat playOrStopAnimatedStickers(false); } } + + @Override + public void seekTo(long positionMs) { + super.seekTo(positionMs); + if (isCurrentVideo) { + seekAnimatedStickersTo(positionMs); + } + } }; newPlayerCreated = true; } @@ -5616,7 +6238,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat aspectRatioFrameLayout.setAspectRatio(height == 0 ? 1 : (width * pixelWidthHeightRatio) / height, unappliedRotationDegrees); if (videoTextureView instanceof VideoEditTextureView) { ((VideoEditTextureView) videoTextureView).setVideoSize((int) (width * pixelWidthHeightRatio), height); + if (sendPhotoType == SELECT_TYPE_AVATAR) { + setCropBitmap(); + } } + videoSizeSet = true; } } @@ -5628,6 +6254,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + @Override + public void onRenderedFirstFrame(AnalyticsListener.EventTime eventTime) { + if (eventTime.eventPlaybackPositionMs == needCaptureFrameReadyAtTime) { + captureFrameReadyAtTime = eventTime.eventPlaybackPositionMs; + needCaptureFrameReadyAtTime = -1; + captureCurrentFrame(); + } + } + @Override public boolean onSurfaceDestroyed(SurfaceTexture surfaceTexture) { if (changingTextureView) { @@ -5675,13 +6310,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat ObjectAnimator.ofFloat(videoTextureView, View.SCALE_Y, 1.0f), ObjectAnimator.ofFloat(videoTextureView, View.TRANSLATION_X, pipPosition[0] - aspectRatioFrameLayout.getX()), ObjectAnimator.ofFloat(videoTextureView, View.TRANSLATION_Y, pipPosition[1] - aspectRatioFrameLayout.getY()), - ObjectAnimator.ofInt(backgroundDrawable, AnimationProperties.COLOR_DRAWABLE_ALPHA, 255), - ObjectAnimator.ofFloat(actionBar, View.ALPHA, 1.0f), - ObjectAnimator.ofFloat(bottomLayout, View.ALPHA, 1.0f), - ObjectAnimator.ofFloat(captionTextViewSwitcher, View.ALPHA, 1.0f), - ObjectAnimator.ofFloat(groupedPhotosListView, View.ALPHA, 1.0f) + ObjectAnimator.ofInt(backgroundDrawable, AnimationProperties.COLOR_DRAWABLE_ALPHA, 255) ); - animatorSet.setInterpolator(new DecelerateInterpolator()); + final DecelerateInterpolator interpolator = new DecelerateInterpolator(); + animatorSet.setInterpolator(interpolator); animatorSet.setDuration(250); animatorSet.addListener(new AnimatorListenerAdapter() { @Override @@ -5690,7 +6322,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } }); animatorSet.start(); + toggleActionBar(true, true, new ActionBarToggleParams().enableStatusBarAnimation(false).animationDuration(250).animationInterpolator(interpolator)); } else { + toggleActionBar(true, false); //containerView.setTranslationY(0); } @@ -5793,6 +6427,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat layoutParams.width = getMeasuredWidth(); layoutParams.height = getMeasuredHeight(); } + if (videoTextureView instanceof VideoEditTextureView) { + videoTextureView.setPivotX(videoTextureView.getMeasuredWidth() / 2); + } else { + videoTextureView.setPivotX(0); + } } }; aspectRatioFrameLayout.setVisibility(View.INVISIBLE); @@ -5810,16 +6449,25 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (injectingVideoPlayerSurface != null) { videoTextureView.setSurfaceTexture(injectingVideoPlayerSurface); textureUploaded = true; + videoSizeSet = true; injectingVideoPlayerSurface = null; } videoTextureView.setPivotX(0); videoTextureView.setPivotY(0); videoTextureView.setOpaque(false); aspectRatioFrameLayout.addView(videoTextureView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.CENTER)); + + if (sendPhotoType == SELECT_TYPE_AVATAR) { + flashView = new View(parentActivity); + flashView.setBackgroundColor(0xffffffff); + flashView.setAlpha(0.0f); + aspectRatioFrameLayout.addView(flashView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.CENTER)); + } } private void releasePlayer(boolean onClose) { if (videoPlayer != null) { + cancelVideoPlayRunnable(); AndroidUtilities.cancelRunOnUIThread(setLoadingRunnable); AndroidUtilities.cancelRunOnUIThread(hideActionBarRunnable); if (shouldSavePositionForCurrentVideoShortTerm != null) { @@ -5855,6 +6503,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } aspectRatioFrameLayout = null; } + cancelFlashAnimations(); + flashView = null; if (videoTextureView != null) { if (videoTextureView instanceof VideoEditTextureView) { ((VideoEditTextureView) videoTextureView).release(); @@ -5977,7 +6627,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private static final int thumbSize = 512; - private void mergeThumbs(String finalPath, String thumbPath, Bitmap thumb, Bitmap bitmap, boolean reverse) { + private void mergeImages(String finalPath, String thumbPath, Bitmap thumb, Bitmap bitmap, float size, boolean reverse) { try { boolean recycle = false; if (thumb == null) { @@ -5986,7 +6636,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } int w = thumb.getWidth(); int h = thumb.getHeight(); - float size = thumbSize; if (w > size || h > size) { float scale = Math.max(w, h) / size; w /= scale; @@ -6003,7 +6652,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat canvas.drawBitmap(bitmap, null, dstRect, bitmapPaint); } FileOutputStream stream = new FileOutputStream(new File(finalPath)); - dst.compress(Bitmap.CompressFormat.JPEG, 83, stream); + dst.compress(Bitmap.CompressFormat.JPEG, size == thumbSize ? 83 : 87, stream); try { stream.close(); } catch (Exception e) { @@ -6018,10 +6667,26 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + private void seekAnimatedStickersTo(long ms) { + if (editState.mediaEntities != null) { + for (int a = 0, N = editState.mediaEntities.size(); a < N; a++) { + VideoEditedInfo.MediaEntity entity = editState.mediaEntities.get(a); + if (entity.type == 0 && (entity.subType & 1) != 0 && entity.view instanceof BackupImageView) { + ImageReceiver imageReceiver = ((BackupImageView) entity.view).getImageReceiver(); + RLottieDrawable drawable = imageReceiver.getLottieAnimation(); + if (drawable == null) { + continue; + } + drawable.setProgressMs(ms - (startTime > 0 ? startTime / 1000 : 0)); + } + } + } + } + private void playOrStopAnimatedStickers(boolean play) { - if (currentMediaEntities != null) { - for (int a = 0, N = currentMediaEntities.size(); a < N; a++) { - VideoEditedInfo.MediaEntity entity = currentMediaEntities.get(a); + if (editState.mediaEntities != null) { + for (int a = 0, N = editState.mediaEntities.size(); a < N; a++) { + VideoEditedInfo.MediaEntity entity = editState.mediaEntities.get(a); if (entity.type == 0 && (entity.subType & 1) != 0 && entity.view instanceof BackupImageView) { ImageReceiver imageReceiver = ((BackupImageView) entity.view).getImageReceiver(); RLottieDrawable drawable = imageReceiver.getLottieAnimation(); @@ -6038,268 +6703,375 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - private boolean hasAnimatedMediaEntities() { - if (currentMediaEntities != null) { - for (int a = 0, N = currentMediaEntities.size(); a < N; a++) { - VideoEditedInfo.MediaEntity entity = currentMediaEntities.get(a); + private int getAnimatedMediaEntitiesCount(boolean single) { + int count = 0; + if (editState.mediaEntities != null) { + for (int a = 0, N = editState.mediaEntities.size(); a < N; a++) { + VideoEditedInfo.MediaEntity entity = editState.mediaEntities.get(a); if (entity.type == 0 && (entity.subType & 1) != 0) { - return true; + count++; + if (single) { + break; + } } } } - return false; + return count; + } + + private boolean hasAnimatedMediaEntities() { + return getAnimatedMediaEntitiesCount(true) != 0; + } + + private Bitmap createCroppedBitmap(Bitmap bitmap, MediaController.CropState cropState, int[] extraTransform, boolean mirror) { + try { + int tr = cropState.transformRotation + (extraTransform != null ? extraTransform[0] : 0); + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int fw = w, rotatedW = w; + int fh = h, rotatedH = h; + if (tr == 90 || tr == 270) { + int temp = fw; + fw = rotatedW = fh; + fh = rotatedH = temp; + } + fw *= cropState.cropPw; + fh *= cropState.cropPh; + Bitmap canvasBitmap = Bitmap.createBitmap(fw, fh, Bitmap.Config.ARGB_8888); + Matrix matrix = new Matrix(); + matrix.postTranslate(-w / 2, -h / 2); + if (mirror && cropState.mirrored) { + if (tr == 90 || tr == 270) { + matrix.postScale(1, -1); + } else { + matrix.postScale(-1, 1); + } + } + matrix.postRotate(cropState.cropRotate + tr); + matrix.postTranslate(cropState.cropPx * rotatedW, cropState.cropPy * rotatedH); + matrix.postScale(cropState.cropScale, cropState.cropScale); + matrix.postTranslate(fw / 2, fh / 2); + Canvas canvas = new Canvas(canvasBitmap); + canvas.drawBitmap(bitmap, matrix, new Paint(Paint.FILTER_BITMAP_FLAG)); + return canvasBitmap; + } catch (Throwable e) { + FileLog.e(e); + } + return null; } private void applyCurrentEditMode() { + if (currentIndex < 0 || currentIndex >= imagesArrLocals.size()) { + return; + } Bitmap bitmap = null; Bitmap[] paintThumbBitmap = new Bitmap[1]; ArrayList stickers = null; MediaController.SavedFilterState savedFilterState = null; ArrayList entities = null; + int[] orientation = null; + boolean hasChanged = false; MediaController.MediaEditState entry = (MediaController.MediaEditState) imagesArrLocals.get(currentIndex); if (currentEditMode == 1 || currentEditMode == 0 && sendPhotoType == SELECT_TYPE_AVATAR) { - bitmap = photoCropView.getBitmap(entry); + photoCropView.makeCrop(entry); + if (entry.cropState == null) { + return; + } + if (isCurrentVideo) { + if (!TextUtils.isEmpty(entry.filterPath)) { + bitmap = ImageLoader.loadBitmap(entry.filterPath, null, thumbSize, thumbSize, true); + } else { + if (sendPhotoType == SELECT_TYPE_AVATAR) { + orientation = new int[1]; + bitmap = SendMessagesHelper.createVideoThumbnailAtTime(entry.getPath(), avatarStartTime / 1000, orientation, true); + } else { + bitmap = SendMessagesHelper.createVideoThumbnailAtTime(entry.getPath(), (long) (videoTimelineView.getLeftProgress() * videoPlayer.getDuration() * 1000L)); + } + } + } else { + bitmap = centerImage.getBitmap(); + orientation = new int[]{centerImage.getOrientation()}; + } } else if (currentEditMode == 2) { bitmap = photoFilterView.getBitmap(); savedFilterState = photoFilterView.getSavedFilterState(); } else if (currentEditMode == 3) { - if (Build.VERSION.SDK_INT >= 18 && (sendPhotoType == 0 || sendPhotoType == 2)) { + if (Build.VERSION.SDK_INT >= 18 && (sendPhotoType == 0 || sendPhotoType == SELECT_TYPE_AVATAR || sendPhotoType == 2)) { entities = new ArrayList<>(); } + hasChanged = photoPaintView.hasChanges(); bitmap = photoPaintView.getBitmap(entities, paintThumbBitmap); stickers = photoPaintView.getMasks(); } - if (bitmap != null) { - TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(bitmap, currentEditMode == 3 ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); - if (size != null) { - if (entry.thumbPath != null) { - new File(entry.thumbPath).delete(); - entry.thumbPath = null; - } + if (bitmap == null) { + return; + } - if (currentEditMode == 1 || currentEditMode == 0 && sendPhotoType == SELECT_TYPE_AVATAR) { - if (entry.filterPath == null) { - if (entry.croppedPath != null) { - new File(entry.croppedPath).delete(); - } - entry.croppedPath = currentImagePath = FileLoader.getPathToAttach(size, true).toString(); - } else { - if (entry.filterPath != null) { - new File(entry.filterPath).delete(); - } - entry.filterPath = currentImagePath = FileLoader.getPathToAttach(size, true).toString(); - } - if (entry.imagePath != null) { - new File(entry.imagePath).delete(); - } - if (entry.paintPath != null) { - File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); - mergeThumbs(entry.thumbPath = f.getAbsolutePath(), entry.fullPaintPath, null, bitmap, true); + if (entry.thumbPath != null) { + new File(entry.thumbPath).delete(); + entry.thumbPath = null; + } + if (entry.imagePath != null) { + new File(entry.imagePath).delete(); + entry.imagePath = null; + } - Bitmap paintBitmap = BitmapFactory.decodeFile(entry.paintPath); - paintingOverlay.setBitmap(paintBitmap); - if (entry.fullPaintPath != null && !entry.paintPath.equals(entry.fullPaintPath)) { - paintBitmap = BitmapFactory.decodeFile(hasAnimatedMediaEntities() ? entry.paintPath : entry.fullPaintPath); - } - Bitmap canvasBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); - Canvas b = new Canvas(canvasBitmap); - b.drawBitmap(bitmap, null, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), bitmapPaint); - b.drawBitmap(paintBitmap, null, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), bitmapPaint); - size = ImageLoader.scaleAndSaveImage(canvasBitmap, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); - entry.imagePath = FileLoader.getPathToAttach(size, true).toString(); - canvasBitmap.recycle(); - paintingOverlay.setEntities(currentMediaEntities, isCurrentVideo, true); - } else { - size = ImageLoader.scaleAndSaveImage(bitmap, thumbSize, thumbSize, 70, false, 101, 101); - entry.thumbPath = FileLoader.getPathToAttach(size, true).toString(); + if (currentEditMode == 1 || currentEditMode == 0 && sendPhotoType == SELECT_TYPE_AVATAR) { + editState.cropState = entry.cropState; + editState.croppedPaintPath = entry.croppedPaintPath; + editState.croppedMediaEntities = entry.croppedMediaEntities; - try { - File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); - AndroidUtilities.copyFile(new File(entry.croppedPath), f); - entry.imagePath = f.getAbsolutePath(); - } catch (Exception e) { - FileLog.e(e); - } - } - } else if (currentEditMode == 2) { - if (entry.filterPath != null) { - new File(entry.filterPath).delete(); - } - entry.filterPath = currentImagePath = FileLoader.getPathToAttach(size, true).toString(); - if (entry.paintPath != null && !hasAnimatedMediaEntities() && !isCurrentVideo) { - Bitmap paintBitmap; - boolean recycle; - if (entry.paintPath.equals(entry.fullPaintPath)) { - paintBitmap = paintingOverlay.getBitmap(); - recycle = false; - } else { - paintBitmap = BitmapFactory.decodeFile(entry.fullPaintPath); - recycle = true; - } - Bitmap canvasBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); - Canvas b = new Canvas(canvasBitmap); - b.drawBitmap(bitmap, null, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), bitmapPaint); - b.drawBitmap(paintBitmap, null, new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()), bitmapPaint); - size = ImageLoader.scaleAndSaveImage(canvasBitmap, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); - if (entry.imagePath != null) { - new File(entry.imagePath).delete(); - } - entry.imagePath = FileLoader.getPathToAttach(size, true).toString(); + Bitmap croppedBitmap = createCroppedBitmap(bitmap, entry.cropState, orientation, true); + if (entry.paintPath != null) { + Bitmap paintBitmap = BitmapFactory.decodeFile(entry.fullPaintPath); + Bitmap croppedPaintBitmap = createCroppedBitmap(paintBitmap, entry.cropState, null, false); - size = ImageLoader.scaleAndSaveImage(canvasBitmap, thumbSize, thumbSize, 70, false, 101, 101); - entry.thumbPath = FileLoader.getPathToAttach(size, true).toString(); - if (recycle) { - paintBitmap.recycle(); - } - canvasBitmap.recycle(); - } else if (entry.paintPath == null) { - try { - File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); - AndroidUtilities.copyFile(new File(entry.filterPath), f); - entry.thumbPath = f.getAbsolutePath(); - } catch (Exception e) { - FileLog.e(e); - } - if (!hasAnimatedMediaEntities()) { - if (entry.imagePath != null) { - new File(entry.imagePath).delete(); - } - try { - File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); - AndroidUtilities.copyFile(new File(entry.filterPath), f); - entry.imagePath = f.getAbsolutePath(); - } catch (Exception e) { - FileLog.e(e); - } - } + if (!isCurrentVideo) { + if (hasAnimatedMediaEntities()) { + TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(croppedBitmap, Bitmap.CompressFormat.JPEG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); + entry.imagePath = currentImagePath = FileLoader.getPathToAttach(size, true).toString(); } else { File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); - mergeThumbs(entry.thumbPath = f.getAbsolutePath(), entry.fullPaintPath, entry.paintPath.equals(entry.fullPaintPath) ? paintingOverlay.getThumb() : null, paintThumbBitmap[0] != null ? paintThumbBitmap[0] : bitmap, true); + mergeImages(entry.imagePath = f.getAbsolutePath(), null, croppedPaintBitmap, croppedBitmap, AndroidUtilities.getPhotoSize(), true); } - } else if (currentEditMode == 3) { - if (entry.paintPath != null) { - new File(entry.paintPath).delete(); - if (!entry.paintPath.equals(entry.fullPaintPath)) { - new File(entry.fullPaintPath).delete(); - } - } - entry.stickers = stickers; - entry.paintPath = currentPaintPath = FileLoader.getPathToAttach(size, true).toString(); - paintingOverlay.setEntities(entry.mediaEntities = currentMediaEntities = entities == null || entities.isEmpty() ? null : entities, isCurrentVideo, true); - entry.averageDuration = currentAverageDuration = photoPaintView.getLcm(); - if (entry.mediaEntities != null && paintThumbBitmap[0] != null) { - size = ImageLoader.scaleAndSaveImage(paintThumbBitmap[0], Bitmap.CompressFormat.PNG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); - entry.fullPaintPath = FileLoader.getPathToAttach(size, true).toString(); + } + File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); + mergeImages(entry.thumbPath = f.getAbsolutePath(), null, croppedPaintBitmap, croppedBitmap, thumbSize, true); + croppedPaintBitmap.recycle(); + paintBitmap.recycle(); + } else { + if (!isCurrentVideo) { + TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(croppedBitmap, Bitmap.CompressFormat.JPEG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); + entry.imagePath = currentImagePath = FileLoader.getPathToAttach(size, true).toString(); + } + TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(croppedBitmap, thumbSize, thumbSize, 70, false, 101, 101); + entry.thumbPath = FileLoader.getPathToAttach(size, true).toString(); + } + if (currentEditMode == 0 && isCurrentVideo) { + bitmap.recycle(); + bitmap = croppedBitmap; + } + } else if (currentEditMode == 2) { + if (entry.filterPath != null) { + new File(entry.filterPath).delete(); + } + TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(bitmap, Bitmap.CompressFormat.JPEG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); + entry.filterPath = FileLoader.getPathToAttach(size, true).toString(); + Bitmap b = entry.cropState != null ? createCroppedBitmap(bitmap, entry.cropState, null, true) : bitmap; + if (entry.paintPath == null) { + if (!isCurrentVideo) { + size = ImageLoader.scaleAndSaveImage(b, Bitmap.CompressFormat.JPEG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); + entry.imagePath = currentImagePath = FileLoader.getPathToAttach(size, true).toString(); + } + size = ImageLoader.scaleAndSaveImage(b, Bitmap.CompressFormat.JPEG, thumbSize, thumbSize, 83, false, 101, 101); + entry.thumbPath = FileLoader.getPathToAttach(size, true).toString(); + } else { + String path = entry.fullPaintPath; + Bitmap fullPaintBitmap = entry.paintPath.equals(entry.fullPaintPath) ? paintingOverlay.getThumb() : null; + Bitmap paintBitmap; + boolean recyclePaint; + if (entry.cropState != null) { + if (fullPaintBitmap == null) { + Bitmap b2 = BitmapFactory.decodeFile(entry.fullPaintPath); + paintBitmap = createCroppedBitmap(b2, entry.cropState, null, false); + b2.recycle(); } else { - entry.fullPaintPath = entry.paintPath; + paintBitmap = createCroppedBitmap(fullPaintBitmap, entry.cropState, null, false); } - paintingOverlay.setBitmap(bitmap); - - if (!hasAnimatedMediaEntities() && !isCurrentVideo) { - String path; - if (entry.filterPath != null) { - path = entry.filterPath; - } else if (entry.croppedPath != null) { - path = entry.croppedPath; - } else { - path = entry.getPath(); - } - Bitmap originalBitmap = ImageLoader.loadBitmap(path, null, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), true); - if (originalBitmap != null && !originalBitmap.isMutable()) { - originalBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true); - } - if (originalBitmap != null) { - Canvas b = new Canvas(originalBitmap); - b.drawBitmap(paintThumbBitmap[0] != null ? paintThumbBitmap[0] : bitmap, null, new Rect(0, 0, originalBitmap.getWidth(), originalBitmap.getHeight()), bitmapPaint); - size = ImageLoader.scaleAndSaveImage(originalBitmap, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); - if (entry.imagePath != null) { - new File(entry.imagePath).delete(); - } - entry.imagePath = FileLoader.getPathToAttach(size, true).toString(); - - size = ImageLoader.scaleAndSaveImage(originalBitmap, thumbSize, thumbSize, 70, false, 101, 101); - entry.thumbPath = FileLoader.getPathToAttach(size, true).toString(); - } - } else if (entry.filterPath == null) { - if (entry.imagePath != null) { - new File(entry.imagePath).delete(); - entry.imagePath = null; - } - Bitmap originalBitmap; - if (isCurrentVideo) { - VideoEditTextureView textureView = (VideoEditTextureView) videoTextureView; - originalBitmap = SendMessagesHelper.createVideoThumbnailAtTime(entry.getPath(), (long) (videoTimelineView.getLeftProgress() * videoPlayer.getDuration() * 1000L)); - } else { - String path; - if (entry.croppedPath != null) { - path = entry.croppedPath; - } else { - path = entry.getPath(); - } - originalBitmap = ImageLoader.loadBitmap(path, null, thumbSize, thumbSize, true); - if (originalBitmap != null && !originalBitmap.isMutable()) { - originalBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true); - } - } - if (originalBitmap != null) { - Canvas b = new Canvas(originalBitmap); - b.drawBitmap(paintThumbBitmap[0] != null ? paintThumbBitmap[0] : bitmap, null, new Rect(0, 0, originalBitmap.getWidth(), originalBitmap.getHeight()), bitmapPaint); - size = ImageLoader.scaleAndSaveImage(originalBitmap, thumbSize, thumbSize, 70, false, 101, 101); - entry.thumbPath = FileLoader.getPathToAttach(size, true).toString(); - } + recyclePaint = true; + } else { + paintBitmap = fullPaintBitmap; + recyclePaint = false; + } + if (!isCurrentVideo) { + if (hasAnimatedMediaEntities()) { + size = ImageLoader.scaleAndSaveImage(b, Bitmap.CompressFormat.JPEG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); + entry.imagePath = currentImagePath = FileLoader.getPathToAttach(size, true).toString(); } else { File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); - mergeThumbs(entry.thumbPath = f.getAbsolutePath(), entry.filterPath, null, paintThumbBitmap[0] != null ? paintThumbBitmap[0] : bitmap, false); + mergeImages(entry.imagePath = f.getAbsolutePath(), path, paintBitmap, b, AndroidUtilities.getPhotoSize(), true); } } - - SharedConfig.saveConfig(); - - if (savedFilterState != null) { - entry.savedFilterState = currentSavedFilterState = savedFilterState; - } - if (currentEditMode == 1) { - entry.isCropped = true; - cropItem.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.SRC_IN)); - } else if (currentEditMode == 2) { - entry.isFiltered = true; - tuneItem.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.SRC_IN)); - } else if (currentEditMode == 3) { - entry.isPainted = true; - paintItem.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.SRC_IN)); - } - - if ((sendPhotoType == 0 || sendPhotoType == 4) && placeProvider != null) { - placeProvider.updatePhotoAtIndex(currentIndex); - if (!placeProvider.isPhotoChecked(currentIndex)) { - setPhotoChecked(); - } - } - if (currentEditMode == 1) { - float scaleX = photoCropView.getRectSizeX() / (float) getContainerViewWidth(); - float scaleY = photoCropView.getRectSizeY() / (float) getContainerViewHeight(); - scale = Math.max(scaleX, scaleY); - translationX = photoCropView.getRectX() + photoCropView.getRectSizeX() / 2 - getContainerViewWidth() / 2; - translationY = photoCropView.getRectY() + photoCropView.getRectSizeY() / 2 - getContainerViewHeight() / 2; - zoomAnimation = true; - applying = true; - - photoCropView.onDisappear(); - } - centerImage.setParentView(null); - ignoreDidSetImage = true; - if (!isCurrentVideo && currentEditMode != 3) { - centerImage.setImageBitmap(bitmap); - centerImage.setOrientation(0, true); - containerView.requestLayout(); - } - ignoreDidSetImage = false; - centerImage.setParentView(containerView); - if (sendPhotoType == SELECT_TYPE_AVATAR) { - setCropBitmap(); + File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); + mergeImages(entry.thumbPath = f.getAbsolutePath(), path, paintBitmap, b, thumbSize, true); + if (recyclePaint) { + paintBitmap.recycle(); } } + if (entry.cropState != null) { + b.recycle(); + } + } else if (currentEditMode == 3) { + if (entry.paintPath != null) { + new File(entry.paintPath).delete(); + if (!entry.paintPath.equals(entry.fullPaintPath)) { + new File(entry.fullPaintPath).delete(); + } + } + TLRPC.PhotoSize size = ImageLoader.scaleAndSaveImage(bitmap, Bitmap.CompressFormat.PNG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); + + entry.stickers = stickers; + entry.paintPath = editState.paintPath = FileLoader.getPathToAttach(size, true).toString(); + paintingOverlay.setEntities(entry.mediaEntities = editState.mediaEntities = entities == null || entities.isEmpty() ? null : entities, isCurrentVideo, true); + entry.averageDuration = editState.averageDuration = photoPaintView.getLcm(); + if (entry.mediaEntities != null && paintThumbBitmap[0] != null) { + size = ImageLoader.scaleAndSaveImage(paintThumbBitmap[0], Bitmap.CompressFormat.PNG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); + entry.fullPaintPath = FileLoader.getPathToAttach(size, true).toString(); + } else { + entry.fullPaintPath = entry.paintPath; + } + paintingOverlay.setBitmap(bitmap); + + if (entry.cropState != null && entry.cropState.initied) { + String path = CropView.getCopy(editState.paintPath); + if (editState.croppedPaintPath != null) { + new File(editState.croppedPaintPath).delete(); + editState.croppedPaintPath = null; + } + editState.croppedPaintPath = path; + if (editState.mediaEntities != null && !editState.mediaEntities.isEmpty()) { + editState.croppedMediaEntities = new ArrayList<>(editState.mediaEntities.size()); + for (int a = 0, N = editState.mediaEntities.size(); a < N; a++) { + editState.croppedMediaEntities.add(editState.mediaEntities.get(a).copy()); + } + } else { + editState.croppedMediaEntities = null; + } + Bitmap resultBitmap = Bitmap.createBitmap(entry.cropState.width, entry.cropState.height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(resultBitmap); + int w, h; + if (isCurrentVideo) { + VideoEditTextureView videoEditTextureView = (VideoEditTextureView) videoTextureView; + w = videoEditTextureView.getVideoWidth(); + h = videoEditTextureView.getVideoHeight(); + } else { + w = centerImage.getBitmapWidth(); + h = centerImage.getBitmapHeight(); + } + CropView.editBitmap(parentActivity, path, null, canvas, resultBitmap, Bitmap.CompressFormat.PNG, entry.cropState.matrix, w, h, entry.cropState.stateScale, entry.cropState.cropRotate, entry.cropState.transformRotation, entry.cropState.scale, false, editState.croppedMediaEntities, false); + resultBitmap.recycle(); + + entry.croppedPaintPath = editState.croppedPaintPath; + entry.croppedMediaEntities = editState.croppedMediaEntities; + } + + boolean recycle = false; + Bitmap fullPaintBitmap = paintThumbBitmap[0] != null ? paintThumbBitmap[0] : bitmap; + Bitmap paintBitmap = fullPaintBitmap; + if (entry.cropState != null && entry.cropState.initied) { + paintBitmap = createCroppedBitmap(fullPaintBitmap, entry.cropState, null, false); + recycle = true; + } + + boolean recycleCropped; + Bitmap croppedBitmap; + if (isCurrentVideo) { + Bitmap originalBitmap; + if (entry.filterPath == null) { + originalBitmap = SendMessagesHelper.createVideoThumbnailAtTime(entry.getPath(), (long) (videoTimelineView.getLeftProgress() * videoPlayer.getDuration() * 1000L)); + } else { + originalBitmap = ImageLoader.loadBitmap(entry.filterPath, null, thumbSize, thumbSize, true); + } + croppedBitmap = entry.cropState != null ? createCroppedBitmap(originalBitmap, entry.cropState, null, true) : originalBitmap; + if (entry.cropState != null) { + originalBitmap.recycle(); + } + recycleCropped = true; + } else { + orientation = new int[]{centerImage.getOrientation()}; + if (entry.cropState != null) { + croppedBitmap = createCroppedBitmap(centerImage.getBitmap(), entry.cropState, orientation, true); + recycleCropped = true; + } else { + croppedBitmap = centerImage.getBitmap(); + if (orientation[0] != 0) { + Matrix matrix = new Matrix(); + matrix.postRotate(orientation[0]); + croppedBitmap = Bitmaps.createBitmap(croppedBitmap, 0, 0, croppedBitmap.getWidth(), croppedBitmap.getHeight(), matrix, true); + recycleCropped = true; + } else { + recycleCropped = false; + } + } + } + if (!isCurrentVideo) { + if (hasAnimatedMediaEntities()) { + size = ImageLoader.scaleAndSaveImage(croppedBitmap, Bitmap.CompressFormat.JPEG, AndroidUtilities.getPhotoSize(), AndroidUtilities.getPhotoSize(), 87, false, 101, 101); + entry.imagePath = currentImagePath = FileLoader.getPathToAttach(size, true).toString(); + } else { + File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); + mergeImages(entry.imagePath = f.getAbsolutePath(), null, croppedBitmap, paintBitmap, AndroidUtilities.getPhotoSize(), false); + } + } + File f = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), SharedConfig.getLastLocalId() + "_temp.jpg"); + mergeImages(entry.thumbPath = f.getAbsolutePath(), null, croppedBitmap, paintBitmap, thumbSize, false); + + if (recycle && paintBitmap != null) { + paintBitmap.recycle(); + } + if (recycleCropped) { + croppedBitmap.recycle(); + } + if (sendPhotoType == SELECT_TYPE_AVATAR && videoPlayer != null && isCurrentVideo && getAnimatedMediaEntitiesCount(false) == 1 && videoTimelineView.getLeftProgress() <= 0.0f && videoTimelineView.getRightProgress() >= 1.0f) { + long duration = entry.averageDuration; + long videoDuration = videoPlayer.getDuration(); + while (duration < 3000 && duration < videoDuration) { + duration += entry.averageDuration; + } + videoTimelineView.setRightProgress(Math.min(1.0f, duration / (float) videoDuration)); + } } + + SharedConfig.saveConfig(); + + if (savedFilterState != null) { + entry.savedFilterState = editState.savedFilterState = savedFilterState; + } + if (currentEditMode == 1) { + entry.isCropped = true; + cropItem.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.SRC_IN)); + } else if (currentEditMode == 2) { + entry.isFiltered = true; + tuneItem.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.SRC_IN)); + } else if (currentEditMode == 3) { + if (hasChanged) { + entry.isPainted = true; + paintItem.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogFloatingButton), PorterDuff.Mode.SRC_IN)); + } + } + + if ((sendPhotoType == 0 || sendPhotoType == 4) && placeProvider != null) { + placeProvider.updatePhotoAtIndex(currentIndex); + if (!placeProvider.isPhotoChecked(currentIndex)) { + setPhotoChecked(); + } + } + if (currentEditMode == 1) { + float scaleX = photoCropView.getRectSizeX() / (float) getContainerViewWidth(); + float scaleY = photoCropView.getRectSizeY() / (float) getContainerViewHeight(); + scale = Math.max(scaleX, scaleY); + translationX = photoCropView.getRectX() + photoCropView.getRectSizeX() / 2 - getContainerViewWidth() / 2; + translationY = photoCropView.getRectY() + photoCropView.getRectSizeY() / 2 - getContainerViewHeight() / 2; + zoomAnimation = true; + applying = true; + + photoCropView.onDisappear(); + } + centerImage.setParentView(null); + ignoreDidSetImage = true; + boolean setImage; + if (isCurrentVideo) { + setImage = currentEditMode == 1 || currentEditMode == 0 && sendPhotoType == SELECT_TYPE_AVATAR; + } else { + setImage = currentEditMode == 2; + } + if (setImage) { + centerImage.setImageBitmap(bitmap); + centerImage.setOrientation(0, true); + containerView.requestLayout(); + } + ignoreDidSetImage = false; + centerImage.setParentView(containerView); } private void setPhotoChecked() { @@ -6335,49 +7107,162 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (photoCropView != null) { return; } - photoCropView = new PhotoCropView(actvityContext); + photoCropView = new PhotoCropView(activityContext); photoCropView.setVisibility(View.GONE); - int index = containerView.indexOfChild(pickerViewSendButton); + photoCropView.onDisappear(); + int index = containerView.indexOfChild(videoTimelineView); containerView.addView(photoCropView, index - 1, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 0, 0, 0, 48)); - photoCropView.setDelegate(reset -> resetButton.setVisibility(reset ? View.GONE : View.VISIBLE)); + photoCropView.setDelegate(new PhotoCropView.PhotoCropViewDelegate() { + @Override + public void onChange(boolean reset) { + resetButton.setVisibility(reset ? View.GONE : View.VISIBLE); + } + + @Override + public void onUpdate() { + containerView.invalidate(); + } + + @Override + public void onTapUp() { + if (sendPhotoType == SELECT_TYPE_AVATAR) { + manuallyPaused = true; + toggleVideoPlayer(); + } + } + + @Override + public void onVideoThumbClick() { + if (videoPlayer == null) { + return; + } + videoPlayer.seekTo((long) (videoPlayer.getDuration() * avatarStartProgress)); + videoPlayer.pause(); + videoTimelineView.setProgress(avatarStartProgress); + cancelVideoPlayRunnable(); + AndroidUtilities.runOnUIThread(videoPlayRunnable = () -> { + manuallyPaused = false; + if (videoPlayer != null) { + videoPlayer.play(); + } + videoPlayRunnable = null; + }, 860); + } + + @Override + public int getVideoThumbX() { + return (int) (AndroidUtilities.dp(16) + (videoTimelineView.getMeasuredWidth() - AndroidUtilities.dp(32)) * avatarStartProgress); + } + }); + } + + private void startVideoPlayer() { + if (isCurrentVideo && videoPlayer != null && !videoPlayer.isPlaying()) { + if (!muteVideo || sendPhotoType == SELECT_TYPE_AVATAR) { + videoPlayer.setVolume(0); + } + manuallyPaused = false; + toggleVideoPlayer(); + } + } + + private void detectFaces() { } private void switchToEditMode(final int mode) { if (currentEditMode == mode || (isCurrentVideo && photoProgressViews[0].backgroundState != 3) && !isCurrentVideo && (centerImage.getBitmap() == null || photoProgressViews[0].backgroundState != -1) || changeModeAnimation != null || imageMoveAnimation != null || captionEditText.getTag() != null) { return; } + switchingToMode = mode; if (mode == 0) { // no edit mode Bitmap bitmap = centerImage.getBitmap(); if (bitmap != null) { int bitmapWidth = centerImage.getBitmapWidth(); int bitmapHeight = centerImage.getBitmapHeight(); - float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; - float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; - float newScaleX = (float) getContainerViewWidth(0) / (float) bitmapWidth; - float newScaleY = (float) getContainerViewHeight(0) / (float) bitmapHeight; - float scale = Math.min(scaleX, scaleY); - float newScale = Math.min(newScaleX, newScaleY); - - if (sendPhotoType == SELECT_TYPE_AVATAR) { - setCropTranslations(true); + float newScale; + float oldScale; + if (currentEditMode == 3) { + if (sendPhotoType != SELECT_TYPE_AVATAR) { + if (cropTransform.getOrientation() == 90 || cropTransform.getOrientation() == 270) { + int temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + } + } else { + if (editState.cropState != null) { + if (editState.cropState.transformRotation == 90 || editState.cropState.transformRotation == 270) { + int temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + } + bitmapWidth *= editState.cropState.cropPw; + bitmapHeight *= editState.cropState.cropPh; + } + } + newScale = Math.min(getContainerViewWidth(0) / (float) bitmapWidth, getContainerViewHeight(0) / (float) bitmapHeight); + oldScale = Math.min(getContainerViewWidth() / (float) bitmapWidth, getContainerViewHeight() / (float) bitmapHeight); + } else { + if (currentEditMode != 1 && editState.cropState != null && (editState.cropState.transformRotation == 90 || editState.cropState.transformRotation == 270)) { + float scaleToFitX = getContainerViewWidth() / (float) bitmapHeight; + if (scaleToFitX * bitmapWidth > getContainerViewHeight()) { + scaleToFitX = getContainerViewHeight() / (float) bitmapWidth; + } + float sc = Math.min(getContainerViewWidth() / (float) bitmapWidth, getContainerViewHeight() / (float) bitmapHeight); + float cropScale = scaleToFitX / sc; + scale = 1.0f / cropScale; + } else if (sendPhotoType == SELECT_TYPE_AVATAR && (cropTransform.getOrientation() == 90 || cropTransform.getOrientation() == 270)) { + float scaleToFitX = getContainerViewWidth() / (float) bitmapHeight; + if (scaleToFitX * bitmapWidth > getContainerViewHeight()) { + scaleToFitX = getContainerViewHeight() / (float) bitmapWidth; + } + float sc = Math.min(getContainerViewWidth() / (float) bitmapWidth, getContainerViewHeight() / (float) bitmapHeight); + float cropScale = cropTransform.getScale() / cropTransform.getTrueCropScale() * scaleToFitX / sc / cropTransform.getMinScale(); + scale = 1.0f / cropScale; + } + if (editState.cropState != null) { + if (editState.cropState.transformRotation == 90 || editState.cropState.transformRotation == 270) { + int temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + } + bitmapWidth *= editState.cropState.cropPw; + bitmapHeight *= editState.cropState.cropPh; + } else if (sendPhotoType == SELECT_TYPE_AVATAR && (cropTransform.getOrientation() == 90 || cropTransform.getOrientation() == 270)) { + int temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + } + oldScale = Math.min(getContainerViewWidth() / (float) bitmapWidth, getContainerViewHeight() / (float) bitmapHeight); + if (sendPhotoType == SELECT_TYPE_AVATAR) { + newScale = getCropFillScale(cropTransform.getOrientation() == 90 || cropTransform.getOrientation() == 270); + } else { + newScale = Math.min(getContainerViewWidth(0) / (float) bitmapWidth, getContainerViewHeight(0) / (float) bitmapHeight); + } + } + animateToScale = newScale / oldScale; + animateToX = 0; + translationX = getLeftInset() / 2 - getRightInset() / 2; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + if (currentEditMode == 2) { + animateToY = AndroidUtilities.dp(36); + } else if (currentEditMode == 3) { + animateToY = -AndroidUtilities.dp(12); + } } else { - animateToScale = newScale / scale; - animateToX = 0; - translationX = getLeftInset() / 2 - getRightInset() / 2; if (currentEditMode == 1) { - animateToY = AndroidUtilities.dp(24 + 34); + animateToY = AndroidUtilities.dp(24 + 32); } else if (currentEditMode == 2) { - animateToY = AndroidUtilities.dp(92); + animateToY = AndroidUtilities.dp(93); } else if (currentEditMode == 3) { animateToY = AndroidUtilities.dp(44); } - if (Build.VERSION.SDK_INT >= 21) { + if (isStatusBarVisible()) { animateToY -= AndroidUtilities.statusBarHeight / 2; } - animationStartTime = System.currentTimeMillis(); - zoomAnimation = true; } + animationStartTime = System.currentTimeMillis(); + zoomAnimation = true; } padImageForHorizontalInsets = false; @@ -6407,26 +7292,36 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat @Override public void onAnimationEnd(Animator animation) { if (currentEditMode == 1) { + photoCropView.onDisappear(); + photoCropView.onHide(); editorDoneLayout.setVisibility(View.GONE); photoCropView.setVisibility(View.GONE); } else if (currentEditMode == 2) { - containerView.removeView(photoFilterView); + try { + containerView.removeView(photoFilterView); + } catch (Exception e) { + FileLog.e(e); + } photoFilterView = null; } else if (currentEditMode == 3) { - containerView.removeView(photoPaintView); + try { + containerView.removeView(photoPaintView); + } catch (Exception e) { + FileLog.e(e); + } photoPaintView = null; } imageMoveAnimation = null; currentEditMode = mode; + switchingToMode = -1; applying = false; if (sendPhotoType == SELECT_TYPE_AVATAR) { photoCropView.setVisibility(View.VISIBLE); - } else { - animateToScale = 1; - animateToX = 0; - animateToY = 0; - scale = 1; } + animateToScale = 1; + animateToX = 0; + animateToY = 0; + scale = 1; updateMinMax(scale); containerView.invalidate(); @@ -6468,8 +7363,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (sendPhotoType == 0 || sendPhotoType == 4 || (sendPhotoType == 2 || sendPhotoType == 5) && imagesArrLocals.size() > 1) { checkImageView.setVisibility(View.VISIBLE); photosCounterView.setVisibility(View.VISIBLE); - } else if (sendPhotoType == SELECT_TYPE_AVATAR) { - setCropTranslations(false); } } }); @@ -6478,7 +7371,17 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat }); imageMoveAnimation.start(); } else if (mode == 1) { // crop + startVideoPlayer(); createCropView(); + previousHasTransform = cropTransform.hasViewTransform(); + previousCropPx = cropTransform.getCropPx(); + previousCropPy = cropTransform.getCropPy(); + previousCropScale = cropTransform.getScale(); + previousCropRotation = cropTransform.getRotation(); + previousCropOrientation = cropTransform.getOrientation(); + previousCropPw = cropTransform.getCropPw(); + previousCropPh = cropTransform.getCropPh(); + previousCropMirrored = cropTransform.isMirrored(); photoCropView.onAppear(); editorDoneLayout.doneButton.setText(LocaleController.getString("Crop", R.string.Crop)); @@ -6530,11 +7433,20 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } final Bitmap bitmap = centerImage.getBitmap(); - if (bitmap != null) { - photoCropView.setBitmap(bitmap, centerImage.getOrientation(), sendPhotoType != SELECT_TYPE_AVATAR, false, paintingOverlay); + if (bitmap != null || isCurrentVideo) { + photoCropView.setBitmap(bitmap, centerImage.getOrientation(), sendPhotoType != SELECT_TYPE_AVATAR, false, paintingOverlay, cropTransform, isCurrentVideo ? (VideoEditTextureView) videoTextureView : null, editState.cropState); photoCropView.onDisappear(); int bitmapWidth = centerImage.getBitmapWidth(); int bitmapHeight = centerImage.getBitmapHeight(); + if (editState.cropState != null) { + if (editState.cropState.transformRotation == 90 || editState.cropState.transformRotation == 270) { + int temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + } + bitmapWidth *= editState.cropState.cropPw; + bitmapHeight *= editState.cropState.cropPh; + } float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; @@ -6551,7 +7463,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animateToScale = newScale / scale; animateToX = getLeftInset() / 2 - getRightInset() / 2; - animateToY = -AndroidUtilities.dp(24 + 32) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight / 2 : 0); + animateToY = -AndroidUtilities.dp(24 + 32) + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight / 2 : 0); animationStartTime = System.currentTimeMillis(); zoomAnimation = true; } @@ -6573,9 +7485,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat @Override public void onAnimationEnd(Animator animation) { photoCropView.onAppeared(); + photoCropView.onShow(); imageMoveAnimation = null; currentEditMode = mode; + switchingToMode = -1; animateToScale = 1; animateToX = 0; animateToY = 0; @@ -6590,12 +7504,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat }); changeModeAnimation.start(); } else if (mode == 2) { // filter - if (isCurrentVideo && videoPlayer != null && !videoPlayer.isPlaying()) { - if (!muteVideo) { - videoPlayer.setVolume(0); - } - toggleVideoPlayer(); - } + startVideoPlayer(); if (photoFilterView == null) { MediaController.SavedFilterState state = null; Bitmap bitmap; @@ -6609,12 +7518,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } MediaController.MediaEditState editState = (MediaController.MediaEditState) object; state = editState.savedFilterState; - if (editState.croppedPath != null) { - originalPath = editState.croppedPath; - orientation = 0; - } else { - originalPath = editState.getPath(); - } + originalPath = editState.getPath(); } if (videoTextureView != null) { bitmap = null; @@ -6627,7 +7531,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - photoFilterView = new PhotoFilterView(parentActivity, videoTextureView != null ? (VideoEditTextureView) videoTextureView : null, bitmap, orientation, state, isCurrentVideo ? null : paintingOverlay); + int hasFaces; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + hasFaces = 1; + } else if (isCurrentVideo || currentImageHasFace == 2) { + hasFaces = 2; + } else { + hasFaces = currentImageHasFace == 1 ? 1 : 0; + } + photoFilterView = new PhotoFilterView(parentActivity, videoTextureView != null ? (VideoEditTextureView) videoTextureView : null, bitmap, orientation, state, isCurrentVideo ? null : paintingOverlay, hasFaces, videoTextureView == null && (editState.cropState != null && editState.cropState.mirrored || cropTransform.isMirrored())); containerView.addView(photoFilterView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); photoFilterView.getDoneTextView().setOnClickListener(v -> { applyCurrentEditMode(); @@ -6704,16 +7616,22 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int bitmapWidth = centerImage.getBitmapWidth(); int bitmapHeight = centerImage.getBitmapHeight(); - float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; - float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; - float newScaleX = (float) getContainerViewWidth(2) / (float) bitmapWidth; - float newScaleY = (float) getContainerViewHeight(2) / (float) bitmapHeight; - float scale = Math.min(scaleX, scaleY); - float newScale = Math.min(newScaleX, newScaleY); + float oldScale; + float newScale = Math.min(getContainerViewWidth(2) / (float) bitmapWidth, getContainerViewHeight(2) / (float) bitmapHeight); + if (sendPhotoType == SELECT_TYPE_AVATAR) { + animateToY = -AndroidUtilities.dp(36); + oldScale = getCropFillScale(false); + } else { + animateToY = -AndroidUtilities.dp(93) + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight / 2 : 0); + if (editState.cropState != null && (editState.cropState.transformRotation == 90 || editState.cropState.transformRotation == 270)) { + oldScale = Math.min(getContainerViewWidth() / (float) bitmapHeight, getContainerViewHeight() / (float) bitmapWidth); + } else { + oldScale = Math.min(getContainerViewWidth() / (float) bitmapWidth, getContainerViewHeight() / (float) bitmapHeight); + } + } + animateToScale = newScale / oldScale; - animateToScale = newScale / scale; animateToX = getLeftInset() / 2 - getRightInset() / 2; - animateToY = -AndroidUtilities.dp(92) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight / 2 : 0); animationStartTime = System.currentTimeMillis(); zoomAnimation = true; } @@ -6735,6 +7653,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat photoFilterView.init(); imageMoveAnimation = null; currentEditMode = mode; + switchingToMode = -1; animateToScale = 1; animateToX = 0; animateToY = 0; @@ -6742,9 +7661,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat updateMinMax(scale); padImageForHorizontalInsets = true; containerView.invalidate(); - if (sendPhotoType == SELECT_TYPE_AVATAR) { - photoCropView.reset(); - } } }); imageMoveAnimation.start(); @@ -6752,12 +7668,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat }); changeModeAnimation.start(); } else if (mode == 3) { - if (isCurrentVideo && videoPlayer != null && !videoPlayer.isPlaying()) { - if (!muteVideo) { - videoPlayer.setVolume(0); - } - toggleVideoPlayer(); - } + startVideoPlayer(); if (photoPaintView == null) { int w; int h; @@ -6777,16 +7688,34 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (bitmap == null) { bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); } - photoPaintView = new PhotoPaintView(parentActivity, bitmap, isCurrentVideo ? null : centerImage.getBitmap(), centerImage.getOrientation(), currentMediaEntities, () -> paintingOverlay.hideBitmap()) { + MediaController.CropState state; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + state = new MediaController.CropState(); + state.transformRotation = cropTransform.getOrientation(); + } else { + state = editState.cropState; + } + photoPaintView = new PhotoPaintView(parentActivity, bitmap, isCurrentVideo ? null : centerImage.getBitmap(), centerImage.getOrientation(), editState.mediaEntities, state, () -> paintingOverlay.hideBitmap()) { @Override protected void onOpenCloseStickersAlert(boolean open) { - if (videoPlayer != null) { - if (open) { - videoPlayer.pause(); - } else { - videoPlayer.play(); - } + if (videoPlayer == null) { + return; } + manuallyPaused = false; + cancelVideoPlayRunnable(); + if (open) { + videoPlayer.pause(); + } else { + videoPlayer.play(); + } + } + + @Override + protected void didSetAnimatedSticker(RLottieDrawable drawable) { + if (videoPlayer == null) { + return; + } + drawable.setProgressMs(videoPlayer.getCurrentPosition() - (startTime > 0 ? startTime / 1000 : 0)); } }; containerView.addView(photoPaintView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); @@ -6798,7 +7727,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat photoPaintView.getColorPicker().setTranslationY(AndroidUtilities.dp(126)); photoPaintView.getToolsView().setTranslationY(AndroidUtilities.dp(126)); } - changeModeAnimation = new AnimatorSet(); ArrayList arrayList = new ArrayList<>(); arrayList.add(ObjectAnimator.ofFloat(pickerView, View.TRANSLATION_Y, 0, AndroidUtilities.dp(isCurrentVideo ? 154 : 96))); @@ -6855,16 +7783,31 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int bitmapWidth = centerImage.getBitmapWidth(); int bitmapHeight = centerImage.getBitmapHeight(); - float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; - float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; - float newScaleX = (float) getContainerViewWidth(3) / (float) bitmapWidth; - float newScaleY = (float) getContainerViewHeight(3) / (float) bitmapHeight; - float scale = Math.min(scaleX, scaleY); - float newScale = Math.min(newScaleX, newScaleY); - - animateToScale = newScale / scale; + float oldScale; + float newScale; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + animateToY = AndroidUtilities.dp(12); + if (cropTransform.getOrientation() == 90 || cropTransform.getOrientation() == 270) { + int temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + } + } else { + animateToY = -AndroidUtilities.dp(44) + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight / 2 : 0); + if (editState.cropState != null) { + if (editState.cropState.transformRotation == 90 || editState.cropState.transformRotation == 270) { + int temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + } + bitmapWidth *= editState.cropState.cropPw; + bitmapHeight *= editState.cropState.cropPh; + } + } + oldScale = Math.min(getContainerViewWidth() / (float) bitmapWidth, getContainerViewHeight() / (float) bitmapHeight); + newScale = Math.min(getContainerViewWidth(3) / (float) bitmapWidth, getContainerViewHeight(3) / (float) bitmapHeight); + animateToScale = newScale / oldScale; animateToX = getLeftInset() / 2 - getRightInset() / 2; - animateToY = -AndroidUtilities.dp(44) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight / 2 : 0); animationStartTime = System.currentTimeMillis(); zoomAnimation = true; } @@ -6888,6 +7831,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat paintingOverlay.hideEntities(); imageMoveAnimation = null; currentEditMode = mode; + switchingToMode = -1; animateToScale = 1; animateToX = 0; animateToY = 0; @@ -6895,9 +7839,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat updateMinMax(scale); padImageForHorizontalInsets = true; containerView.invalidate(); - if (sendPhotoType == SELECT_TYPE_AVATAR) { - photoCropView.reset(); - } } }); imageMoveAnimation.start(); @@ -6998,7 +7939,38 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + private static class ActionBarToggleParams { + + public static final ActionBarToggleParams DEFAULT = new ActionBarToggleParams(); + + public int animationDuration = 200; + public Interpolator animationInterpolator; + public boolean enableStatusBarAnimation = true; + + public ActionBarToggleParams() { + } + + public ActionBarToggleParams enableStatusBarAnimation(boolean val) { + enableStatusBarAnimation = val; + return this; + } + + public ActionBarToggleParams animationDuration(int val) { + animationDuration = val; + return this; + } + + public ActionBarToggleParams animationInterpolator(Interpolator val) { + animationInterpolator = val; + return this; + } + } + private void toggleActionBar(final boolean show, final boolean animated) { + toggleActionBar(show, animated, ActionBarToggleParams.DEFAULT); + } + + private void toggleActionBar(final boolean show, final boolean animated, @NonNull final ActionBarToggleParams params) { if (actionBarAnimator != null) { actionBarAnimator.cancel(); } @@ -7015,7 +7987,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } isActionBarVisible = show; - updateContainerFlags(show); + + if (params.enableStatusBarAnimation) { + updateContainerFlags(show); + } if (videoPlayerControlVisible && isPlaying && show) { scheduleActionBarHide(); @@ -7025,13 +8000,24 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat final float offsetY = AndroidUtilities.dpf2(24); + videoPlayerControlFrameLayout.setSeekBarTransitionEnabled(params.enableStatusBarAnimation && playerLooping); + videoPlayerControlFrameLayout.setTranslationYAnimationEnabled(params.enableStatusBarAnimation); + if (animated) { ArrayList arrayList = new ArrayList<>(); arrayList.add(ObjectAnimator.ofFloat(actionBar, View.ALPHA, show ? 1.0f : 0.0f)); - arrayList.add(ObjectAnimator.ofFloat(actionBar, View.TRANSLATION_Y, show ? 0 : -offsetY)); + if (params.enableStatusBarAnimation) { + arrayList.add(ObjectAnimator.ofFloat(actionBar, View.TRANSLATION_Y, show ? 0 : -offsetY)); + } else { + actionBar.setTranslationY(0); + } if (bottomLayout != null) { arrayList.add(ObjectAnimator.ofFloat(bottomLayout, View.ALPHA, show ? 1.0f : 0.0f)); - arrayList.add(ObjectAnimator.ofFloat(bottomLayout, View.TRANSLATION_Y, show ? 0 : offsetY)); + if (params.enableStatusBarAnimation) { + arrayList.add(ObjectAnimator.ofFloat(bottomLayout, View.TRANSLATION_Y, show ? 0 : offsetY)); + } else { + bottomLayout.setTranslationY(0); + } } if (videoPlayerControlVisible) { arrayList.add(ObjectAnimator.ofFloat(videoPlayerControlFrameLayout, VPC_PROGRESS, show ? 1.0f : 0.0f)); @@ -7039,10 +8025,18 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoPlayerControlFrameLayout.setProgress(show ? 1.0f : 0.0f); } arrayList.add(ObjectAnimator.ofFloat(groupedPhotosListView, View.ALPHA, show ? 1.0f : 0.0f)); - arrayList.add(ObjectAnimator.ofFloat(groupedPhotosListView, View.TRANSLATION_Y, show ? 0 : offsetY)); + if (params.enableStatusBarAnimation) { + arrayList.add(ObjectAnimator.ofFloat(groupedPhotosListView, View.TRANSLATION_Y, show ? 0 : offsetY)); + } else { + groupedPhotosListView.setTranslationY(0); + } if (!needCaptionLayout && captionScrollView != null) { arrayList.add(ObjectAnimator.ofFloat(captionScrollView, View.ALPHA, show ? 1.0f : 0.0f)); - arrayList.add(ObjectAnimator.ofFloat(captionScrollView, View.TRANSLATION_Y, show ? 0 : offsetY)); + if (params.enableStatusBarAnimation) { + arrayList.add(ObjectAnimator.ofFloat(captionScrollView, View.TRANSLATION_Y, show ? 0 : offsetY)); + } else { + captionScrollView.setTranslationY(0); + } } if (videoPlayerControlVisible && isPlaying) { final ValueAnimator anim = ValueAnimator.ofFloat(photoProgressViews[0].animAlphas[1], show ? 1.0f : 0.0f); @@ -7054,7 +8048,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } actionBarAnimator = new AnimatorSet(); actionBarAnimator.playTogether(arrayList); - actionBarAnimator.setDuration(200); + actionBarAnimator.setDuration(params.animationDuration); + actionBarAnimator.setInterpolator(params.animationInterpolator); actionBarAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -7145,16 +8140,17 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (videoPlayer == null) { return; } + cancelVideoPlayRunnable(); AndroidUtilities.cancelRunOnUIThread(hideActionBarRunnable); if (isPlaying) { videoPlayer.pause(); } else { if (isCurrentVideo) { - if (Math.abs(videoTimelineView.getProgress() - 1.0f) < 0.01f || videoPlayer.getCurrentPosition() == videoPlayer.getDuration()) { + if (Math.abs(videoTimelineView.getProgress() - videoTimelineView.getRightProgress()) < 0.01f || videoPlayer.getCurrentPosition() == videoPlayer.getDuration()) { videoPlayer.seekTo((int) (videoTimelineView.getLeftProgress() * videoPlayer.getDuration())); } } else { - if (Math.abs(videoPlayerSeekbar.getProgress() - 1.0f) < 0.01f || videoPlayer.getCurrentPosition() == videoPlayer.getDuration()) { + if (Math.abs(videoPlayerSeekbar.getProgress() - videoTimelineView.getRightProgress()) < 0.01f || videoPlayer.getCurrentPosition() == videoPlayer.getDuration()) { videoPlayer.seekTo(0); } scheduleActionBarHide(); @@ -7180,10 +8176,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return null; } ImageLocation location = imagesArrLocations.get(index); + ImageLocation videoLocation = imagesArrLocationsVideo.get(index); if (location == null) { return null; } - return location.location.volume_id + "_" + location.location.local_id + ".jpg"; + if (videoLocation != location) { + return videoLocation.location.volume_id + "_" + videoLocation.location.local_id + ".mp4"; + } else { + return location.location.volume_id + "_" + location.location.local_id + ".jpg"; + } } else if (!imagesArr.isEmpty()) { if (index >= imagesArr.size()) { return null; @@ -7232,7 +8233,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (size != null) { size[0] = imagesArrLocationsSizes.get(index); } - return imagesArrLocations.get(index); + return imagesArrLocationsVideo.get(index); } else if (!imagesArr.isEmpty()) { if (index >= imagesArr.size()) { return null; @@ -7313,7 +8314,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (size != null) { size[0] = imagesArrLocationsSizes.get(index); } - return imagesArrLocations.get(index).location; + return imagesArrLocationsVideo.get(index).location; } else if (!imagesArr.isEmpty()) { if (index >= imagesArr.size()) { return null; @@ -7377,6 +8378,28 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + private boolean isCurrentAvatarSet() { + if (currentAvatarLocation == null || currentIndex < 0 || currentIndex >= avatarsArr.size()) { + return false; + } + TLRPC.Photo photo = avatarsArr.get(currentIndex); + ImageLocation currentLocation = imagesArrLocations.get(currentIndex); + if (photo instanceof TLRPC.TL_photoEmpty) { + photo = null; + } + if (photo != null) { + for (int a = 0, N = photo.sizes.size(); a < N; a++) { + TLRPC.PhotoSize size = photo.sizes.get(a); + if (size.location != null && size.location.local_id == currentAvatarLocation.location.local_id && size.location.volume_id == currentAvatarLocation.location.volume_id) { + return true; + } + } + } else if (currentLocation.location.local_id == currentAvatarLocation.location.local_id && currentLocation.location.volume_id == currentAvatarLocation.location.volume_id) { + return true; + } + return false; + } + private void setItemVisible(View itemView, boolean visible, boolean animate) { setItemVisible(itemView, visible, animate, 1.0f); } @@ -7403,19 +8426,23 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - private void onPhotoShow(final MessageObject messageObject, final TLRPC.FileLocation fileLocation, ImageLocation imageLocation, final ArrayList messages, final ArrayList documents, final ArrayList photos, int index, final PlaceProviderObject object) { + private void onPhotoShow(final MessageObject messageObject, final TLRPC.FileLocation fileLocation, ImageLocation imageLocation, ImageLocation videoLocation, final ArrayList messages, final ArrayList documents, final ArrayList photos, int index, final PlaceProviderObject object) { classGuid = ConnectionsManager.generateClassGuid(); currentMessageObject = null; currentFileLocation = null; + currentFileLocationVideo = null; currentSecureDocument = null; currentPathObject = null; fromCamera = false; currentBotInlineResult = null; + avatarStartProgress = 0.0f; + avatarStartTime = 0; currentIndex = -1; currentFileNames[0] = null; currentFileNames[1] = null; currentFileNames[2] = null; avatarsDialogId = 0; + canEditAvatar = false; totalImagesCount = 0; totalImagesCountMerge = 0; currentEditMode = 0; @@ -7432,6 +8459,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat imagesArr.clear(); imagesArrLocations.clear(); imagesArrLocationsSizes.clear(); + imagesArrLocationsVideo.clear(); + imagesArrMessages.clear(); avatarsArr.clear(); secureDocuments.clear(); imagesArrLocals.clear(); @@ -7440,11 +8469,18 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat imagesByIdsTemp[a].clear(); } imagesArrTemp.clear(); - currentUserAvatarLocation = null; + currentAvatarLocation = null; containerView.setPadding(0, 0, 0, 0); if (currentThumb != null) { currentThumb.release(); } + if (videoTimelineView != null) { + if (sendPhotoType == SELECT_TYPE_AVATAR) { + videoTimelineView.setBackground(null); + } else { + videoTimelineView.setBackgroundColor(0x7f000000); + } + } currentThumb = object != null ? object.thumb : null; isEvent = object != null && object.isEvent; sharedMediaType = MediaDataController.MEDIA_PHOTOVIDEO; @@ -7480,6 +8516,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat menuItem.hideSubItem(gallery_menu_openin); menuItem.hideSubItem(gallery_menu_savegif); menuItem.hideSubItem(gallery_menu_masks2); + menuItem.hideSubItem(gallery_menu_edit_avatar); + menuItem.hideSubItem(gallery_menu_set_as_main); actionBar.setTranslationY(0); checkImageView.setAlpha(1.0f); @@ -7504,11 +8542,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat tuneItem.setTag(null); timeItem.setVisibility(View.GONE); rotateItem.setVisibility(View.GONE); + mirrorItem.setVisibility(View.GONE); pickerView.getLayoutParams().height = LayoutHelper.WRAP_CONTENT; docInfoTextView.setVisibility(View.GONE); docNameTextView.setVisibility(View.GONE); videoTimelineView.setVisibility(View.GONE); + videoAvatarTooltip.setVisibility(View.GONE); compressItem.setVisibility(View.GONE); captionEditText.setVisibility(View.GONE); mentionListView.setVisibility(View.GONE); @@ -7527,12 +8567,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (photoFilterView != null) { photoFilterView.setVisibility(View.GONE); } - for (int a = 0; a < 3; a++) { if (photoProgressViews[a] != null) { photoProgressViews[a].setBackgroundState(PROGRESS_NONE, false); } } + if (groupedPhotosListView != null) { + groupedPhotosListView.reset(); + groupedPhotosListView.setAnimateBackground(!ApplicationLoader.isNetworkOnline()); + } if (messageObject != null && messages == null) { if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && messageObject.messageOwner.media.webpage != null) { @@ -7599,8 +8642,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat secureDocuments.addAll(documents); setImageIndex(index); } else if (fileLocation != null) { - avatarsDialogId = object.dialogId; - if (imageLocation == null) { + avatarsDialogId = object != null ? object.dialogId : 0; + canEditAvatar = object != null && object.canEdit; + if (imageLocation == null && avatarsDialogId != 0) { if (avatarsDialogId > 0) { TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(avatarsDialogId); imageLocation = ImageLocation.getForUser(user, true); @@ -7613,8 +8657,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } imagesArrLocations.add(imageLocation); - currentUserAvatarLocation = imageLocation; - imagesArrLocationsSizes.add(object.size); + imagesArrLocationsVideo.add(videoLocation != null ? videoLocation : imageLocation); + currentAvatarLocation = imageLocation; + imagesArrLocationsSizes.add(object != null ? object.size : 0); + imagesArrMessages.add(null); avatarsArr.add(new TLRPC.TL_photoEmpty()); shareButton.setVisibility(!videoPlayerControlVisible ? View.VISIBLE : View.GONE); allowShare = true; @@ -7668,30 +8714,36 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (sendPhotoType == SELECT_TYPE_QR) { cropItem.setVisibility(View.GONE); rotateItem.setVisibility(View.GONE); + mirrorItem.setVisibility(View.GONE); } else if (isDocumentsPicker) { cropItem.setVisibility(View.GONE); rotateItem.setVisibility(View.GONE); + mirrorItem.setVisibility(View.GONE); docInfoTextView.setVisibility(View.VISIBLE); docNameTextView.setVisibility(View.VISIBLE); pickerView.getLayoutParams().height = AndroidUtilities.dp(84); } else if (((MediaController.PhotoEntry) obj).isVideo) { cropItem.setVisibility(View.GONE); rotateItem.setVisibility(View.GONE); + mirrorItem.setVisibility(View.GONE); bottomLayout.setVisibility(View.VISIBLE); bottomLayout.setTag(1); bottomLayout.setTranslationY(-AndroidUtilities.dp(48)); } else { cropItem.setVisibility(sendPhotoType != SELECT_TYPE_AVATAR ? View.VISIBLE : View.GONE); rotateItem.setVisibility(sendPhotoType != SELECT_TYPE_AVATAR ? View.GONE : View.VISIBLE); + mirrorItem.setVisibility(sendPhotoType != SELECT_TYPE_AVATAR ? View.GONE : View.VISIBLE); } allowCaption = !isDocumentsPicker; } else if (obj instanceof TLRPC.BotInlineResult) { cropItem.setVisibility(View.GONE); rotateItem.setVisibility(View.GONE); + mirrorItem.setVisibility(View.GONE); allowCaption = false; } else { cropItem.setVisibility(obj instanceof MediaController.SearchImage && ((MediaController.SearchImage) obj).type == 0 ? View.VISIBLE : View.GONE); rotateItem.setVisibility(View.GONE); + mirrorItem.setVisibility(View.GONE); allowCaption = cropItem.getVisibility() == View.VISIBLE; } if (parentChatActivity != null && (parentChatActivity.currentEncryptedChat == null || AndroidUtilities.getPeerLayerVersion(parentChatActivity.currentEncryptedChat.layer) >= 46)) { @@ -7761,10 +8813,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat gestureDetector.setOnDoubleTapListener(value ? this : null); } - public boolean isMuteVideo() { - return muteVideo; - } - private void setImages() { if (animationInProgress == 0) { setIndexToImage(centerImage, currentIndex); @@ -7778,8 +8826,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } - menuItem.hideSubItem(gallery_menu_setascurrent); - switchingToIndex = index; boolean isVideo = false; @@ -7804,7 +8850,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat bottomLayout.setTranslationY(AndroidUtilities.dp(48)); captionTextViewSwitcher.setTranslationY(AndroidUtilities.dp(48)); } else { - if (newMessageObject.isNewGif() && ((int) currentDialogId) != 0) { + if (newMessageObject.isNewGif()) { menuItem.showSubItem(gallery_menu_savegif); } if (newMessageObject.canDeleteMessage(parentChatActivity != null && parentChatActivity.isInScheduleMode(), null) && slideshowMessageId == 0) { @@ -7971,9 +9017,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (index < 0 || index >= imagesArrLocations.size()) { return; } - if (avatarsDialogId == UserConfig.getInstance(currentAccount).getClientUserId() && index != 0) { - menuItem.showSubItem(gallery_menu_setascurrent); - } if (avatarsDialogId < 0) { TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(-avatarsDialogId); if (chat != null) { @@ -7994,9 +9037,29 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat String dateString = LocaleController.formatString("formatDateAtTime", R.string.formatDateAtTime, LocaleController.getInstance().formatterYear.format(new Date(date)), LocaleController.getInstance().formatterDay.format(new Date(date))); dateTextView.setText(dateString); } - if (avatarsDialogId == UserConfig.getInstance(currentAccount).getClientUserId() && !avatarsArr.isEmpty()) { - menuItem.showSubItem(gallery_menu_delete); + if (canEditAvatar && !avatarsArr.isEmpty()) { + menuItem.showSubItem(gallery_menu_edit_avatar); + boolean currentSet = isCurrentAvatarSet(); + if (currentSet) { + menuItem.hideSubItem(gallery_menu_set_as_main); + } else { + menuItem.showSubItem(gallery_menu_set_as_main); + } + boolean canDelete; + if (avatarsDialogId > 0) { + canDelete = true; + } else { + TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(-avatarsDialogId); + canDelete = isCurrentAvatarSet() || MessageObject.canDeleteMessage(currentAccount, false, imagesArrMessages.get(index), chat); + } + if (canDelete) { + menuItem.showSubItem(gallery_menu_delete); + } else { + menuItem.hideSubItem(gallery_menu_delete); + } } else { + menuItem.hideSubItem(gallery_menu_edit_avatar); + menuItem.hideSubItem(gallery_menu_set_as_main); menuItem.hideSubItem(gallery_menu_delete); } if (isEvent) { @@ -8023,6 +9086,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat boolean isFiltered = false; boolean isPainted = false; boolean isCropped = false; + MediaController.CropState cropState = null; if (object instanceof TLRPC.BotInlineResult) { TLRPC.BotInlineResult botInlineResult = currentBotInlineResult = ((TLRPC.BotInlineResult) object); if (botInlineResult.document != null) { @@ -8036,9 +9100,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat MediaController.PhotoEntry photoEntry = ((MediaController.PhotoEntry) object); currentPathObject = photoEntry.path; isVideo = photoEntry.isVideo; + cropState = photoEntry.cropState; } else if (object instanceof MediaController.SearchImage) { MediaController.SearchImage searchImage = (MediaController.SearchImage) object; currentPathObject = searchImage.getPathToAttach(); + cropState = searchImage.cropState; if (searchImage.type == 1) { isAnimation = true; } @@ -8060,32 +9126,55 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } processOpenVideo(currentPathObject, isMuted, start, end); - if (isDocumentsPicker) { + if (isDocumentsPicker || Build.VERSION.SDK_INT < 18) { videoTimelineView.setVisibility(View.GONE); + videoAvatarTooltip.setVisibility(View.GONE); + cropItem.setVisibility(View.GONE); + cropItem.setTag(null); + tuneItem.setVisibility(View.GONE); + tuneItem.setTag(null); + paintItem.setVisibility(View.GONE); + paintItem.setTag(null); + rotateItem.setVisibility(View.GONE); + rotateItem.setTag(null); + mirrorItem.setVisibility(View.GONE); + mirrorItem.setTag(null); muteItem.setVisibility(View.GONE); muteItem.setTag(null); compressItem.setVisibility(View.GONE); } else { videoTimelineView.setVisibility(View.VISIBLE); - muteItem.setVisibility(View.VISIBLE); - muteItem.setTag(1); - compressItem.setVisibility(View.VISIBLE); - } - cropItem.setVisibility(View.GONE); - if (isDocumentsPicker || Build.VERSION.SDK_INT < 18) { - tuneItem.setVisibility(View.GONE); - tuneItem.setTag(null); - paintItem.setVisibility(View.GONE); - paintItem.setTag(null); - } else { + if (sendPhotoType != SELECT_TYPE_AVATAR) { + videoAvatarTooltip.setVisibility(View.GONE); + cropItem.setVisibility(View.VISIBLE); + cropItem.setTag(1); + rotateItem.setVisibility(View.GONE); + rotateItem.setTag(null); + mirrorItem.setVisibility(View.GONE); + mirrorItem.setTag(null); + muteItem.setVisibility(View.VISIBLE); + muteItem.setTag(1); + compressItem.setVisibility(View.VISIBLE); + } else { + videoAvatarTooltip.setVisibility(View.VISIBLE); + cropItem.setVisibility(View.GONE); + cropItem.setTag(null); + rotateItem.setVisibility(View.VISIBLE); + rotateItem.setTag(1); + mirrorItem.setVisibility(View.VISIBLE); + mirrorItem.setTag(1); + muteItem.setVisibility(View.GONE); + muteItem.setTag(null); + compressItem.setVisibility(View.GONE); + } tuneItem.setVisibility(View.VISIBLE); tuneItem.setTag(1); paintItem.setVisibility(View.VISIBLE); paintItem.setTag(1); } - rotateItem.setVisibility(View.GONE); } else { videoTimelineView.setVisibility(View.GONE); + videoAvatarTooltip.setVisibility(View.GONE); muteItem.setVisibility(View.GONE); muteItem.setTag(null); if (isCurrentVideo) { @@ -8098,6 +9187,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat paintItem.setTag(null); cropItem.setVisibility(View.GONE); rotateItem.setVisibility(View.GONE); + mirrorItem.setVisibility(View.GONE); tuneItem.setVisibility(View.GONE); tuneItem.setTag(null); } else { @@ -8114,6 +9204,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } cropItem.setVisibility(sendPhotoType != SELECT_TYPE_AVATAR ? View.VISIBLE : View.GONE); rotateItem.setVisibility(sendPhotoType != SELECT_TYPE_AVATAR ? View.GONE : View.VISIBLE); + mirrorItem.setVisibility(sendPhotoType != SELECT_TYPE_AVATAR ? View.GONE : View.VISIBLE); } actionBar.setSubtitle(null); } @@ -8172,17 +9263,30 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat paintItem.setColorFilter(isPainted ? filter : null); cropItem.setColorFilter(isCropped ? filter : null); tuneItem.setColorFilter(isFiltered ? filter : null); + if (fromCamera) { + mirrorItem.setColorFilter(cropState != null && (isCurrentVideo && !cropState.mirrored || !isCurrentVideo && cropState.mirrored) ? filter : null); + } else { + mirrorItem.setColorFilter(cropState != null && cropState.mirrored ? filter : null); + } + rotateItem.setColorFilter(cropState != null && cropState.transformRotation != 0 ? filter : null); } setCurrentCaption(newMessageObject, caption, animateCaption); } - private TLRPC.FileLocation getFileLocation(ImageLocation location) { + public static TLRPC.FileLocation getFileLocation(ImageLocation location) { if (location == null) { return null; } return location.location; } + public static String getFileLocationExt(ImageLocation location) { + if (location == null || location.imageType != FileLoader.IMAGE_TYPE_ANIMATION) { + return null; + } + return "mp4"; + } + private void setImageIndex(int index) { setImageIndex(index, true, false); } @@ -8210,9 +9314,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat boolean isVideo = false; boolean sameImage = false; Uri videoPath = null; - MediaController.SavedFilterState savedFilterState = null; - currentPaintPath = null; - currentMediaEntities = null; + editState.reset(); if (!imagesArr.isEmpty()) { if (currentIndex < 0 || currentIndex >= imagesArr.size()) { @@ -8251,6 +9353,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat sameImage = true; } currentFileLocation = imagesArrLocations.get(index); + currentFileLocationVideo = imagesArrLocationsVideo.get(index); } else if (!imagesArrLocals.isEmpty()) { if (index < 0 || index >= imagesArrLocals.size()) { closePhoto(false, false); @@ -8278,10 +9381,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } isVideo = entry.isVideo; - savedFilterState = entry.savedFilterState; - currentPaintPath = entry.paintPath; - currentAverageDuration = entry.averageDuration; - currentMediaEntities = entry.mediaEntities; + editState.savedFilterState = entry.savedFilterState; + editState.paintPath = entry.paintPath; + editState.croppedPaintPath = entry.croppedPaintPath; + editState.croppedMediaEntities = entry.croppedMediaEntities; + editState.averageDuration = entry.averageDuration; + editState.mediaEntities = entry.mediaEntities; + editState.cropState = entry.cropState; File file = new File(entry.path); videoPath = Uri.fromFile(file); if (isDocumentsPicker) { @@ -8311,17 +9417,35 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else if (object instanceof MediaController.SearchImage) { MediaController.SearchImage searchImage = (MediaController.SearchImage) object; currentPathObject = searchImage.getPathToAttach(); - savedFilterState = searchImage.savedFilterState; - currentPaintPath = searchImage.paintPath; - currentAverageDuration = searchImage.averageDuration; - currentMediaEntities = searchImage.mediaEntities; + editState.savedFilterState = searchImage.savedFilterState; + editState.paintPath = searchImage.paintPath; + editState.croppedPaintPath = searchImage.croppedPaintPath; + editState.croppedMediaEntities = searchImage.croppedMediaEntities; + editState.averageDuration = searchImage.averageDuration; + editState.mediaEntities = searchImage.mediaEntities; + editState.cropState = searchImage.cropState; + } + if (editState.cropState != null) { + previousHasTransform = true; + previousCropPx = editState.cropState.cropPx; + previousCropPy = editState.cropState.cropPy; + previousCropScale = editState.cropState.cropScale; + previousCropRotation = editState.cropState.cropRotate; + previousCropOrientation = editState.cropState.transformRotation; + previousCropPw = editState.cropState.cropPw; + previousCropPh = editState.cropState.cropPh; + previousCropMirrored = editState.cropState.mirrored; + cropTransform.setViewTransform(previousHasTransform, previousCropPx, previousCropPy, previousCropRotation, previousCropOrientation, previousCropScale, 1.0f, 1.0f, previousCropPw, previousCropPh, 0, 0, previousCropMirrored); + } else { + previousHasTransform = false; + cropTransform.setViewTransform(false, previousCropPx, previousCropPy, previousCropRotation, previousCropOrientation, previousCropScale, 1.0f, 1.0f, previousCropPw, previousCropPh, 0, 0, previousCropMirrored); } if (object instanceof MediaController.MediaEditState) { MediaController.MediaEditState state = (MediaController.MediaEditState) object; - if (state.filterPath != null) { + if (hasAnimatedMediaEntities()) { + currentImagePath = state.imagePath; + } else if (state.filterPath != null) { currentImagePath = state.filterPath; - } else if (state.croppedPath != null) { - currentImagePath = state.croppedPath; } else { currentImagePath = currentPathObject; } @@ -8384,16 +9508,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } if (isVideo && videoPath != null) { isStreaming = false; - preparePlayer(videoPath, false, false, savedFilterState); + preparePlayer(videoPath, sendPhotoType == SELECT_TYPE_AVATAR, false, editState.savedFilterState); } if (!imagesArrLocals.isEmpty()) { paintingOverlay.setVisibility(View.VISIBLE); - paintingOverlay.setData(currentPaintPath, currentMediaEntities, isCurrentVideo, false); + paintingOverlay.setData(editState.paintPath, editState.mediaEntities, isCurrentVideo, false); } else { paintingOverlay.setVisibility(View.GONE); - currentPaintPath = null; - currentMediaEntities = null; + editState.reset(); } if (prevIndex == -1) { @@ -8434,6 +9557,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat checkProgress(2, true, false); } } + detectFaces(); } private void setCurrentCaption(MessageObject messageObject, final CharSequence caption, boolean animated) { @@ -8730,12 +9854,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } f2 = new File(FileLoader.getDirectory(FileLoader.MEDIA_DIR_CACHE), currentFileNames[a]); } else if (currentFileLocation != null) { - if (index < 0 || index >= imagesArrLocations.size()) { + if (index < 0 || index >= imagesArrLocationsVideo.size()) { photoProgressViews[a].setBackgroundState(PROGRESS_NONE, animated); return; } - ImageLocation location = imagesArrLocations.get(index); - f1 = FileLoader.getPathToAttach(location.location, avatarsDialogId != 0 || isEvent); + ImageLocation location = imagesArrLocationsVideo.get(index); + f1 = FileLoader.getPathToAttach(location.location, getFileLocationExt(location), avatarsDialogId != 0 || isEvent); } else if (currentSecureDocument != null) { if (index < 0 || index >= secureDocuments.size()) { photoProgressViews[a].setBackgroundState(PROGRESS_NONE, animated); @@ -8751,6 +9875,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat File f2Final = f2; MessageObject messageObjectFinal = messageObject; boolean canStreamFinal = canStream; + boolean canAutoPlayFinal = !(a == 0 && dontAutoPlay) && shouldMessageObjectAutoPlayed(messageObject); boolean isVideoFinal = isVideo; Utilities.globalQueue.postRunnable(() -> { boolean exists = false; @@ -8774,7 +9899,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat AndroidUtilities.runOnUIThread(() -> { if ((f1Final != null || f2Final != null) && (existsFinal || canStreamFinal)) { if (a != 0 || !isPlaying) { - if (isVideoFinal && (!shouldMessageObjectAutoPlayed(messageObjectFinal) || a == 0 && playerWasPlaying)) { + if (isVideoFinal && (!canAutoPlayFinal || a == 0 && playerWasPlaying)) { photoProgressViews[a].setBackgroundState(PROGRESS_PLAY, animated); } else { photoProgressViews[a].setBackgroundState(PROGRESS_NONE, animated); @@ -8876,15 +10001,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat isVideo = photoEntry.isVideo; if (photoEntry.isVideo) { if (photoEntry.thumbPath != null) { - path = photoEntry.thumbPath; + if (fromCamera) { + Bitmap b = BitmapFactory.decodeFile(photoEntry.thumbPath); + if (b != null) { + placeHolder = new ImageReceiver.BitmapHolder(b); + photoEntry.thumbPath = null; + } + } else { + path = photoEntry.thumbPath; + } } else { path = "vthumb://" + photoEntry.imageId + ":" + photoEntry.path; } } else { if (photoEntry.filterPath != null) { path = photoEntry.filterPath; - } else if (photoEntry.croppedPath != null) { - path = photoEntry.croppedPath; } else { imageReceiver.setOrientation(photoEntry.orientation, false); path = photoEntry.path; @@ -8904,7 +10035,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else if (botInlineResult.type.equals("gif") && botInlineResult.document != null) { document = botInlineResult.document; imageSize = botInlineResult.document.size; - TLRPC.TL_videoSize videoSize = MessageObject.getDocumentVideoThumb(botInlineResult.document); + TLRPC.VideoSize videoSize = MessageObject.getDocumentVideoThumb(botInlineResult.document); if (videoSize != null) { videoThumb = ImageLocation.getForDocument(videoSize, document); } @@ -8935,8 +10066,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat imageSize = photoEntry.photoSize.size; } else if (photoEntry.filterPath != null) { path = photoEntry.filterPath; - } else if (photoEntry.croppedPath != null) { - path = photoEntry.croppedPath; } else if (photoEntry.document != null) { document = photoEntry.document; imageSize = photoEntry.document.size; @@ -8986,12 +10115,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat TLRPC.PhotoSize thumbLocation = FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 320); imageReceiver.setNeedsQualityThumb(thumbLocation.w < 100 && thumbLocation.h < 100); imageReceiver.setImage(null, null, placeHolder == null ? ImageLocation.getForObject(thumbLocation, messageObject.photoThumbsObject) : null, "b", placeHolder != null ? new BitmapDrawable(placeHolder.bitmap) : null, 0, null, messageObject, 1); + if (currentThumb != null) { + imageReceiver.setOrientation(currentThumb.orientation, false); + } } else { imageReceiver.setImageBitmap(parentActivity.getResources().getDrawable(R.drawable.photoview_placeholder)); } return; } else if (currentAnimation != null) { - currentAnimation.setSecondParentView(containerView); + currentAnimation.addSecondParentView(containerView); imageReceiver.setImageBitmap(currentAnimation); return; } else if (sharedMediaType == MediaDataController.MEDIA_FILE) { @@ -9015,9 +10147,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int[] size = new int[1]; ImageLocation imageLocation = getImageLocation(index, size); TLObject fileLocation = getFileLocation(index, size); + imageReceiver.setNeedsQualityThumb(true); if (imageLocation != null) { - imageReceiver.setNeedsQualityThumb(true); ImageReceiver.BitmapHolder placeHolder = null; if (currentThumb != null && imageReceiver == centerImage) { placeHolder = currentThumb; @@ -9044,7 +10176,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat parentObject = messageObject; if (sharedMediaType == MediaDataController.MEDIA_GIF) { TLRPC.Document document = messageObject.getDocument(); - TLRPC.TL_videoSize videoSize = MessageObject.getDocumentVideoThumb(document); + TLRPC.VideoSize videoSize = MessageObject.getDocumentVideoThumb(document); if (videoSize != null) { videoThumb = ImageLocation.getForDocument(videoSize, document); } @@ -9061,10 +10193,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (videoThumb != null) { imageReceiver.setImage(imageLocation, null, videoThumb, null, placeHolder == null ? ImageLocation.getForObject(thumbLocation, photoObject) : null, "b", placeHolder != null ? new BitmapDrawable(placeHolder.bitmap) : null, size[0], null, parentObject, cacheOnly ? 1 : 0); } else { - imageReceiver.setImage(imageLocation, null, placeHolder == null ? ImageLocation.getForObject(thumbLocation, photoObject) : null, "b", placeHolder != null ? new BitmapDrawable(placeHolder.bitmap) : null, size[0], null, parentObject, cacheOnly ? 1 : 0); + String filter; + if (avatarsDialogId != 0) { + filter = imageLocation.imageType == FileLoader.IMAGE_TYPE_ANIMATION ? ImageLoader.AUTOPLAY_FILTER : null; + } else { + filter = null; + } + imageReceiver.setImage(imageLocation, filter, placeHolder == null ? ImageLocation.getForObject(thumbLocation, photoObject) : null, "b", placeHolder != null ? new BitmapDrawable(placeHolder.bitmap) : null, size[0], null, parentObject, cacheOnly ? 1 : 0); } } else { - imageReceiver.setNeedsQualityThumb(true); if (size[0] == 0) { imageReceiver.setImageBitmap((Bitmap) null); } else { @@ -9096,7 +10233,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public static boolean isShowingImage(TLRPC.FileLocation object) { boolean result = false; if (Instance != null) { - result = Instance.isVisible && !Instance.disableShowCheck && object != null && Instance.currentFileLocation != null && object.local_id == Instance.currentFileLocation.location.local_id && object.volume_id == Instance.currentFileLocation.location.volume_id && object.dc_id == Instance.currentFileLocation.dc_id; + result = Instance.isVisible && !Instance.disableShowCheck && object != null && + ((Instance.currentFileLocation != null && object.local_id == Instance.currentFileLocation.location.local_id && object.volume_id == Instance.currentFileLocation.location.volume_id && object.dc_id == Instance.currentFileLocation.dc_id) || + (Instance.currentFileLocationVideo != null && object.local_id == Instance.currentFileLocationVideo.location.local_id && object.volume_id == Instance.currentFileLocationVideo.location.volume_id && object.dc_id == Instance.currentFileLocationVideo.dc_id)); } return result; } @@ -9126,39 +10265,52 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat allowOrder = order; } + public void checkCurrentImageVisibility() { + if (currentPlaceObject != null) { + currentPlaceObject.imageReceiver.setVisible(true, true); + } + currentPlaceObject = placeProvider.getPlaceForPhoto(currentMessageObject, getFileLocation(currentFileLocation), currentIndex, false); + if (currentPlaceObject != null) { + currentPlaceObject.imageReceiver.setVisible(false, true); + } + } + public boolean openPhoto(final MessageObject messageObject, long dialogId, long mergeDialogId, final PhotoViewerProvider provider) { - return openPhoto(messageObject, null, null, null, null, null, 0, provider, null, dialogId, mergeDialogId, true); + return openPhoto(messageObject, null, null, null, null, null, null, 0, provider, null, dialogId, mergeDialogId, true); } public boolean openPhoto(final MessageObject messageObject, long dialogId, long mergeDialogId, final PhotoViewerProvider provider, boolean fullScreenVideo) { - return openPhoto(messageObject, null, null, null, null, null, 0, provider, null, dialogId, mergeDialogId, fullScreenVideo); + return openPhoto(messageObject, null, null, null, null, null, null, 0, provider, null, dialogId, mergeDialogId, fullScreenVideo); } public boolean openPhoto(final TLRPC.FileLocation fileLocation, final PhotoViewerProvider provider) { - return openPhoto(null, fileLocation, null, null, null, null, 0, provider, null, 0, 0, true); + return openPhoto(null, fileLocation, null, null, null, null, null, 0, provider, null, 0, 0, true); + } + + public boolean openPhotoWithVideo(final TLRPC.FileLocation fileLocation, ImageLocation videoLocation, final PhotoViewerProvider provider) { + return openPhoto(null, fileLocation, null, videoLocation, null, null, null, 0, provider, null, 0, 0, true); } public boolean openPhoto(final TLRPC.FileLocation fileLocation, final ImageLocation imageLocation, final PhotoViewerProvider provider) { - return openPhoto(null, fileLocation, imageLocation, null, null, null, 0, provider, null, 0, 0, true); + return openPhoto(null, fileLocation, imageLocation, null, null, null, null, 0, provider, null, 0, 0, true); } public boolean openPhoto(final ArrayList messages, final int index, long dialogId, long mergeDialogId, final PhotoViewerProvider provider) { - return openPhoto(messages.get(index), null, null, messages, null, null, index, provider, null, dialogId, mergeDialogId, true); + return openPhoto(messages.get(index), null, null, null, messages, null, null, index, provider, null, dialogId, mergeDialogId, true); } public boolean openPhoto(final ArrayList documents, final int index, final PhotoViewerProvider provider) { - return openPhoto(null, null, null, null, documents, null, index, provider, null, 0, 0, true); + return openPhoto(null, null, null, null, null, documents, null, index, provider, null, 0, 0, true); } public boolean openPhotoForSelect(final ArrayList photos, final int index, int type, boolean documentsPicker, final PhotoViewerProvider provider, ChatActivity chatActivity) { - sendPhotoType = type; isDocumentsPicker = documentsPicker; if (pickerViewSendButton != null) { FrameLayout.LayoutParams layoutParams2 = (FrameLayout.LayoutParams) pickerViewSendButton.getLayoutParams(); - if (sendPhotoType == 4 || sendPhotoType == 5) { + if (type == 4 || type == 5) { pickerViewSendButton.setImageResource(R.drawable.attach_send); layoutParams2.bottomMargin = AndroidUtilities.dp(19); - } else if (sendPhotoType == SELECT_TYPE_AVATAR || sendPhotoType == SELECT_TYPE_WALLPAPER || sendPhotoType == SELECT_TYPE_QR) { + } else if (type == SELECT_TYPE_AVATAR || type == SELECT_TYPE_WALLPAPER || type == SELECT_TYPE_QR) { pickerViewSendButton.setImageResource(R.drawable.floating_check); pickerViewSendButton.setPadding(0, AndroidUtilities.dp(1), 0, 0); layoutParams2.bottomMargin = AndroidUtilities.dp(19); @@ -9168,7 +10320,52 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } pickerViewSendButton.setLayoutParams(layoutParams2); } - return openPhoto(null, null, null, null, null, photos, index, provider, chatActivity, 0, 0, true); + if (sendPhotoType != SELECT_TYPE_AVATAR && type == SELECT_TYPE_AVATAR && isVisible) { + sendPhotoType = type; + doneButtonPressed = false; + actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, 1, 1)); + placeProvider = provider; + mergeDialogId = 0; + currentDialogId = 0; + selectedPhotosAdapter.notifyDataSetChanged(); + + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain(); + } + + isVisible = true; + + togglePhotosListView(false, false); + + openedFullScreenVideo = false; + createCropView(); + toggleActionBar(false, false); + seekToProgressPending2 = 0; + skipFirstBufferingProgress = false; + playerInjected = false; + + if (Build.VERSION.SDK_INT >= 21) { + windowLayoutParams.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | + WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | + WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS; + } else { + windowLayoutParams.flags = 0; + } + windowLayoutParams.softInputMode = (useSmoothKeyboard ? WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN : WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) | WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; + WindowManager wm = (WindowManager) parentActivity.getSystemService(Context.WINDOW_SERVICE); + wm.updateViewLayout(windowView, windowLayoutParams); + windowView.setFocusable(true); + containerView.setFocusable(true); + + backgroundDrawable.setAlpha(255); + containerView.setAlpha(1.0f); + onPhotoShow(null, null, null, null, null, null, photos, index, null); + initCropView(); + setCropBitmap(); + return true; + } + sendPhotoType = type; + return openPhoto(null, null, null, null, null, null, photos, index, provider, chatActivity, 0, 0, true); } private boolean checkAnimation() { @@ -9184,60 +10381,25 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return animationInProgress != 0; } - private void setCropTranslations(boolean animated) { - if (sendPhotoType != SELECT_TYPE_AVATAR) { - return; - } - int bitmapWidth = centerImage.getBitmapWidth(); - int bitmapHeight = centerImage.getBitmapHeight(); - if (bitmapWidth == 0 || bitmapHeight == 0) { - return; - } - - float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; - float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; - float scaleFinal = Math.min(scaleX, scaleY); - - float minSide = Math.min(getContainerViewWidth(1), getContainerViewHeight(1)); - float newScaleX = minSide / (float) bitmapWidth; - float newScaleY = minSide / (float) bitmapHeight; - float newScale = Math.max(newScaleX, newScaleY); - - if (animated) { - animationStartTime = System.currentTimeMillis(); - animateToX = getLeftInset() / 2 - getRightInset() / 2; - if (currentEditMode == 2) { - animateToY = AndroidUtilities.dp(92) - AndroidUtilities.dp(24 + 32); - } else if (currentEditMode == 3) { - animateToY = AndroidUtilities.dp(44) - AndroidUtilities.dp(24 + 32); - } - //animateToY = -AndroidUtilities.dp(24 + 32) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight / 2 : 0); - animateToScale = newScale / scaleFinal; - zoomAnimation = true; - } else { - animationStartTime = 0; - translationX = getLeftInset() / 2 - getRightInset() / 2; - translationY = -AndroidUtilities.dp(24 + 32) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight / 2 : 0); - scale = newScale / scaleFinal; - updateMinMax(scale); - } - } - private void setCropBitmap() { - if (sendPhotoType != SELECT_TYPE_AVATAR) { + if (cropInitied || sendPhotoType != SELECT_TYPE_AVATAR) { return; } + if (isCurrentVideo) { + VideoEditTextureView textureView = (VideoEditTextureView) videoTextureView; + if (textureView == null || textureView.getVideoWidth() <= 0 || textureView.getVideoHeight() <= 0) { + return; + } + } + cropInitied = true; Bitmap bitmap = centerImage.getBitmap(); int orientation = centerImage.getOrientation(); if (bitmap == null) { bitmap = animatingImageView.getBitmap(); orientation = animatingImageView.getOrientation(); } - if (bitmap != null) { - photoCropView.setBitmap(bitmap, orientation, false, false, paintingOverlay); - if (currentEditMode == 0) { - setCropTranslations(false); - } + if (bitmap != null || videoTextureView != null) { + photoCropView.setBitmap(bitmap, orientation, false, false, paintingOverlay, cropTransform, isCurrentVideo ? (VideoEditTextureView) videoTextureView : null, editState.cropState); } } @@ -9245,7 +10407,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (sendPhotoType != SELECT_TYPE_AVATAR) { return; } - photoCropView.setBitmap(null, 0, false, false, null); + photoCropView.setBitmap(null, 0, false, false, null, null, null, null); photoCropView.onAppear(); photoCropView.setVisibility(View.VISIBLE); photoCropView.setAlpha(1.0f); @@ -9253,7 +10415,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat padImageForHorizontalInsets = true; } - public boolean openPhoto(final MessageObject messageObject, final TLRPC.FileLocation fileLocation, final ImageLocation imageLocation, final ArrayList messages, final ArrayList documents, final ArrayList photos, final int index, final PhotoViewerProvider provider, ChatActivity chatActivity, long dialogId, long mDialogId, boolean fullScreenVideo) { + public boolean openPhoto(final MessageObject messageObject, final TLRPC.FileLocation fileLocation, final ImageLocation imageLocation, final ImageLocation videoLocation, final ArrayList messages, final ArrayList documents, final ArrayList photos, final int index, final PhotoViewerProvider provider, ChatActivity chatActivity, long dialogId, long mDialogId, boolean fullScreenVideo) { if (parentActivity == null || isVisible || provider == null && checkAnimation() || messageObject == null && fileLocation == null && messages == null && photos == null && documents == null && imageLocation == null) { return false; } @@ -9355,7 +10517,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - onPhotoShow(messageObject, fileLocation, imageLocation, messages, documents, photos, index, object); + onPhotoShow(messageObject, fileLocation, imageLocation, videoLocation, messages, documents, photos, index, object); if (sendPhotoType == SELECT_TYPE_AVATAR) { photoCropView.setVisibility(View.VISIBLE); photoCropView.setAlpha(0.0f); @@ -9382,7 +10544,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat initCropView(); if (sendPhotoType == SELECT_TYPE_AVATAR) { - photoCropView.hideBackView(); photoCropView.setAspectRatio(1.0f); } @@ -9416,7 +10577,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public boolean onPreDraw() { if (animatingImageViews.length > 1) { animatingImageViews[1].setAlpha(1.0f); + animatingImageViews[1].setAdditionalTranslationX(-getLeftInset()); } + animatingImageViews[0].setTranslationX(animatingImageViews[0].getTranslationX() + getLeftInset()); windowView.getViewTreeObserver().removeOnPreDrawListener(this); float scaleX; float scaleY; @@ -9424,7 +10587,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat float yPos; float xPos; if (sendPhotoType == SELECT_TYPE_AVATAR) { - float statusBarHeight = (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + float statusBarHeight = (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); float measuredHeight = (float) photoCropView.getMeasuredHeight() - AndroidUtilities.dp(64) - statusBarHeight; float minSide = Math.min(photoCropView.getMeasuredWidth(), measuredHeight) - 2 * AndroidUtilities.dp(16); float centerX = photoCropView.getMeasuredWidth() / 2.0f; @@ -9442,9 +10605,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat xPos = (windowView.getMeasuredWidth() - getLeftInset() - getRightInset() - layoutParams.width * scale) / 2.0f + getLeftInset(); } else { scaleX = (float) windowView.getMeasuredWidth() / layoutParams.width; - scaleY = (float) (AndroidUtilities.displaySize.y + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0)) / layoutParams.height; + scaleY = (float) (AndroidUtilities.displaySize.y + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0)) / layoutParams.height; scale = Math.min(scaleX, scaleY); - yPos = ((AndroidUtilities.displaySize.y + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0)) - (layoutParams.height * scale)) / 2.0f; + yPos = ((AndroidUtilities.displaySize.y + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0)) - (layoutParams.height * scale)) / 2.0f; xPos = (windowView.getMeasuredWidth() - layoutParams.width * scale) / 2.0f; } int clipHorizontal = (int) Math.abs(left - object.imageReceiver.getImageX()); @@ -9452,11 +10615,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int[] coords2 = new int[2]; object.parentView.getLocationInWindow(coords2); - int clipTop = (int) (coords2[1] - (Build.VERSION.SDK_INT >= 21 ? 0 : AndroidUtilities.statusBarHeight) - (object.viewY + top) + object.clipTopAddition); + int clipTop = (int) (coords2[1] - (Build.VERSION.SDK_INT >= 21 || inBubbleMode ? 0 : AndroidUtilities.statusBarHeight) - (object.viewY + top) + object.clipTopAddition); if (clipTop < 0) { clipTop = 0; } - int clipBottom = (int) ((object.viewY + top + layoutParams.height) - (coords2[1] + object.parentView.getHeight() - (Build.VERSION.SDK_INT >= 21 ? 0 : AndroidUtilities.statusBarHeight)) + object.clipBottomAddition); + int clipBottom = (int) ((object.viewY + top + layoutParams.height) - (coords2[1] + object.parentView.getHeight() - (Build.VERSION.SDK_INT >= 21 || inBubbleMode ? 0 : AndroidUtilities.statusBarHeight)) + object.clipBottomAddition); if (clipBottom < 0) { clipBottom = 0; } @@ -9508,9 +10671,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat transitionAnimationStartTime = 0; setImages(); setCropBitmap(); - if (sendPhotoType == SELECT_TYPE_AVATAR) { - photoCropView.showBackView(); - } containerView.invalidate(); for (int i = 0; i < animatingImageViews.length; i++) { animatingImageViews[i].setVisibility(View.GONE); @@ -9535,6 +10695,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat windowView.setFocusable(true); containerView.setFocusable(true); } + if (videoPlayer != null && videoPlayer.isPlaying() && isCurrentVideo && !imagesArrLocals.isEmpty()) { + seekAnimatedStickersTo(videoPlayer.getCurrentPosition()); + playOrStopAnimatedStickers(true); + } }; if (!openedFullScreenVideo) { @@ -9613,7 +10777,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat backgroundDrawable.setAlpha(255); containerView.setAlpha(1.0f); - onPhotoShow(messageObject, fileLocation, imageLocation, messages, documents, photos, index, object); + onPhotoShow(messageObject, fileLocation, imageLocation, videoLocation, messages, documents, photos, index, object); initCropView(); setCropBitmap(); } @@ -9645,6 +10809,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat photoPaintView.maybeShowDismissalAlert(this, parentActivity, () -> switchToEditMode(0)); return; } + if (currentEditMode == 1) { + cropTransform.setViewTransform(previousHasTransform, previousCropPx, previousCropPy, previousCropRotation, previousCropOrientation, previousCropScale, 1.0f, 1.0f, previousCropPw, previousCropPh, 0, 0, previousCropMirrored); + } switchToEditMode(0); return; } @@ -9679,8 +10846,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat photoPaintView = null; } currentEditMode = 0; - } else if (sendPhotoType == SELECT_TYPE_AVATAR) { - photoCropView.setVisibility(View.GONE); } if (parentActivity == null || !isInline && !isVisible || checkAnimation() || placeProvider == null) { @@ -9721,7 +10886,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat object.imageReceiver.startAnimation(); } } - releasePlayer(true); + if (!doneButtonPressed) { + releasePlayer(true); + } captionEditText.onDestroy(); parentChatActivity = null; removeObservers(); @@ -9783,14 +10950,32 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat layoutParams.height = 100; } - float scaleX = (float) windowView.getMeasuredWidth() / layoutParams.width; - float scaleY = (float) (AndroidUtilities.displaySize.y + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0)) / layoutParams.height; - float scale2 = Math.min(scaleX, scaleY); + float scaleX; + float scaleY; + float scale2; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + float statusBarHeight = (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); + float measuredHeight = (float) photoCropView.getMeasuredHeight() - AndroidUtilities.dp(64) - statusBarHeight; + float minSide = Math.min(photoCropView.getMeasuredWidth(), measuredHeight) - 2 * AndroidUtilities.dp(16); + scaleX = minSide / layoutParams.width; + scaleY = minSide / layoutParams.height; + scale2 = Math.max(scaleX, scaleY); + } else { + scaleX = (float) windowView.getMeasuredWidth() / layoutParams.width; + scaleY = (float) (AndroidUtilities.displaySize.y + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0)) / layoutParams.height; + scale2 = Math.min(scaleX, scaleY); + } float width = layoutParams.width * scale * scale2; float height = layoutParams.height * scale * scale2; float xPos = (windowView.getMeasuredWidth() - width) / 2.0f; - float yPos = ((AndroidUtilities.displaySize.y + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0)) - height) / 2.0f; - + float yPos; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + float statusBarHeight = (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); + float measuredHeight = (float) photoCropView.getMeasuredHeight() - statusBarHeight; + yPos = (measuredHeight - height) / 2.0f; + } else { + yPos = ((AndroidUtilities.displaySize.y + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0)) - height) / 2.0f; + } for (int i = 0; i < animatingImageViews.length; i++) { animatingImageViews[i].setLayoutParams(layoutParams); animatingImageViews[i].setTranslationX(xPos + translationX); @@ -9851,6 +11036,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } if (animatingImageViews.length > 1) { animators.add(ObjectAnimator.ofFloat(animatingImageView, View.ALPHA, 0f)); + animatingImageViews[1].setAdditionalTranslationX(-getLeftInset()); } animators.add(ObjectAnimator.ofInt(backgroundDrawable, AnimationProperties.COLOR_DRAWABLE_ALPHA, 0)); animators.add(ObjectAnimator.ofFloat(containerView, View.ALPHA, 0.0f)); @@ -9859,7 +11045,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } animatorSet.playTogether(animators); } else { - int h = (AndroidUtilities.displaySize.y + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0)); + int h = (AndroidUtilities.displaySize.y + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0)); animatorSet.playTogether( ObjectAnimator.ofInt(backgroundDrawable, AnimationProperties.COLOR_DRAWABLE_ALPHA, 0), ObjectAnimator.ofFloat(animatingImageView, View.ALPHA, 0.0f), @@ -9931,7 +11117,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatorSet.start(); } if (currentAnimation != null) { - currentAnimation.setSecondParentView(null); + currentAnimation.removeSecondParentView(containerView); currentAnimation = null; centerImage.setImageBitmap((Drawable) null); } @@ -10000,11 +11186,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } private void onPhotoClosed(PlaceProviderObject object) { + if (doneButtonPressed) { + releasePlayer(true); + } isVisible = false; + cropInitied = false; disableShowCheck = true; currentMessageObject = null; currentBotInlineResult = null; currentFileLocation = null; + currentFileLocationVideo = null; currentSecureDocument = null; currentPathObject = null; if (videoPlayerControlFrameLayout != null) { @@ -10013,9 +11204,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (captionScrollView != null) { captionScrollView.reset(); } - if (groupedPhotosListView != null) { - groupedPhotosListView.reset(); - } sendPhotoType = 0; isDocumentsPicker = false; if (currentThumb != null) { @@ -10024,7 +11212,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } parentAlert = null; if (currentAnimation != null) { - currentAnimation.setSecondParentView(null); + currentAnimation.removeSecondParentView(containerView); currentAnimation = null; } for (int a = 0; a < 3; a++) { @@ -10034,8 +11222,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } requestVideoPreview(0); if (videoTimelineView != null) { + videoTimelineView.setBackgroundColor(0x7f000000); videoTimelineView.destroy(); } + hintView.hide(false, 0); centerImage.setImageBitmap((Bitmap) null); leftImage.setImageBitmap((Bitmap) null); rightImage.setImageBitmap((Bitmap) null); @@ -10116,8 +11306,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (currentEditMode == 3 && aspectRatioFrameLayout != null && aspectRatioFrameLayout.getVisibility() == View.VISIBLE && textureUploaded) { scale *= Math.min(getContainerViewWidth() / (float) videoTextureView.getMeasuredWidth(), getContainerViewHeight() / (float) videoTextureView.getMeasuredHeight()); } - int maxW = (int) (centerImage.getImageWidth() * scale - getContainerViewWidth()) / 2; - int maxH = (int) (centerImage.getImageHeight() * scale - getContainerViewHeight()) / 2; + float w = centerImage.getImageWidth(); + float h = centerImage.getImageHeight(); + if (editState.cropState != null) { + w *= editState.cropState.cropPw; + h *= editState.cropState.cropPh; + } + int maxW = (int) (w * scale - getContainerViewWidth()) / 2; + int maxH = (int) (h * scale - getContainerViewHeight()) / 2; if (maxW > 0) { minX = -maxW; @@ -10134,17 +11330,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } private int getAdditionX() { - if (currentEditMode != 0 && currentEditMode != 3) { + if (currentEditMode == 1 || currentEditMode == 0 && sendPhotoType == SELECT_TYPE_AVATAR) { + return AndroidUtilities.dp(16); + } else if (currentEditMode != 0 && currentEditMode != 3) { return AndroidUtilities.dp(14); } return 0; } private int getAdditionY() { - if (currentEditMode == 3) { - return AndroidUtilities.dp(8) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + if (currentEditMode == 1 || currentEditMode == 0 && sendPhotoType == SELECT_TYPE_AVATAR) { + return AndroidUtilities.dp(16) + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); + } else if (currentEditMode == 3) { + return AndroidUtilities.dp(8) + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); } else if (currentEditMode != 0) { - return AndroidUtilities.dp(14) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + return AndroidUtilities.dp(14) + (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); } return 0; } @@ -10155,7 +11355,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private int getContainerViewWidth(int mode) { int width = containerView.getWidth(); - if (mode != 0 && mode != 3) { + if (mode == 1 || mode == 0 && sendPhotoType == SELECT_TYPE_AVATAR) { + width -= AndroidUtilities.dp(32); + } else if (mode != 0 && mode != 3) { width -= AndroidUtilities.dp(28); } return width; @@ -10171,15 +11373,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private int getContainerViewHeight(boolean trueHeight, int mode) { int height; - if (trueHeight) { + if (trueHeight || inBubbleMode) { height = containerView.getMeasuredHeight(); } else { height = AndroidUtilities.displaySize.y; - if (mode == 0 && Build.VERSION.SDK_INT >= 21) { + if (mode == 0 && sendPhotoType != SELECT_TYPE_AVATAR && isStatusBarVisible()) { height += AndroidUtilities.statusBarHeight; } } - if (mode == 1) { + if (mode == 0 && sendPhotoType == SELECT_TYPE_AVATAR || mode == 1) { height -= AndroidUtilities.dp(48 + 32 + 64); } else if (mode == 2) { height -= AndroidUtilities.dp(154 + 60); @@ -10307,9 +11509,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat scale = (float) Math.hypot(ev.getX(1) - ev.getX(0), ev.getY(1) - ev.getY(0)) / pinchStartDistance * pinchStartScale; translationX = (pinchCenterX - getContainerViewWidth() / 2) - ((pinchCenterX - getContainerViewWidth() / 2) - pinchStartX) * (scale / pinchStartScale); translationY = (pinchCenterY - getContainerViewHeight() / 2) - ((pinchCenterY - getContainerViewHeight() / 2) - pinchStartY) * (scale / pinchStartScale); - if (currentEditMode == 3) { - photoPaintView.setTransform(scale, translationX, translationY); - } updateMinMax(scale); containerView.invalidate(); } else if (ev.getPointerCount() == 1) { @@ -10384,10 +11583,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (scale != 1 || currentEditMode != 0) { translationY -= moveDy; } - if (currentEditMode == 3) { - photoPaintView.setTransform(scale, translationX, translationY); - } - containerView.invalidate(); } } else { @@ -10451,7 +11646,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat moving = false; } else if (draggingDown) { if (Math.abs(dragY - ev.getY()) > getContainerViewHeight() / 6.0f) { - closePhoto(true, false); + if (enableSwipeToPiP() && (dragY - ev.getY() > 0)) { + switchToPip(true); + } else { + closePhoto(true, false); + } } else { if (pickerView.getVisibility() == View.VISIBLE) { toggleActionBar(true, true); @@ -10597,6 +11796,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat releasePlayer(false); FileLoader.getInstance(currentAccount).cancelLoadFile(currentMessageObject.getDocument()); } + if (groupedPhotosListView != null) { + groupedPhotosListView.setAnimateBackground(true); + } playerAutoStarted = false; setImageIndex(currentIndex + add, init, true); if (shouldMessageObjectAutoPlayed(currentMessageObject)) { @@ -10610,6 +11812,19 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return messageObject != null && messageObject.isVideo() && (messageObject.mediaExists || messageObject.attachPathExists || messageObject.canStreamVideo() && SharedConfig.streamMedia) && SharedConfig.autoplayVideo; } + private float getCropFillScale(boolean rotated) { + int width = rotated ? centerImage.getBitmapHeight() : centerImage.getBitmapWidth(); + int height = rotated ? centerImage.getBitmapWidth() : centerImage.getBitmapHeight(); + float statusBarHeight = (isStatusBarVisible() ? AndroidUtilities.statusBarHeight : 0); + float measuredHeight = (float) photoCropView.getMeasuredHeight() - AndroidUtilities.dp(64) - statusBarHeight; + float minSide = Math.min(photoCropView.getMeasuredWidth(), measuredHeight) - 2 * AndroidUtilities.dp(16); + return Math.max(minSide / width, minSide / height); + } + + private boolean isStatusBarVisible() { + return Build.VERSION.SDK_INT >= 21 && !inBubbleMode; + } + @SuppressLint({"NewApi", "DrawAllocation"}) private void onDraw(Canvas canvas) { if (animationInProgress == 1 || animationInProgress == 3 || !isVisible && animationInProgress != 2 && !pipAnimationInProgress) { @@ -10690,13 +11905,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat aty = translationY; } } - if (currentEditMode == 3) { - photoPaintView.setTransform(currentScale, currentTranslationX, currentTranslationY); - } + int containerWidth = getContainerViewWidth(); + int containerHeight = getContainerViewHeight(); if (animationInProgress != 2 && !pipAnimationInProgress && !isInline) { if (currentEditMode == 0 && sendPhotoType != SELECT_TYPE_AVATAR && scale == 1 && aty != -1 && !zoomAnimation) { - float maxValue = getContainerViewHeight() / 4.0f; + float maxValue = containerWidth / 4.0f; backgroundDrawable.setAlpha((int) Math.max(127, 255 * (1.0f - (Math.min(Math.abs(aty), maxValue) / maxValue)))); } else { backgroundDrawable.setAlpha(255); @@ -10722,21 +11936,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat float scaleDiff = 0; float alpha = 1; if (!zoomAnimation && translateX < minX) { - alpha = Math.min(1.0f, (minX - translateX) / getContainerViewWidth()); + alpha = Math.min(1.0f, (minX - translateX) / containerWidth); scaleDiff = (1.0f - alpha) * 0.3f; - translateX = -getContainerViewWidth() - AndroidUtilities.dp(30) / 2; + translateX = -containerWidth - AndroidUtilities.dp(30) / 2; } if (sideImage.hasBitmapImage()) { canvas.save(); - canvas.translate(getContainerViewWidth() / 2, getContainerViewHeight() / 2); - canvas.translate(getContainerViewWidth() + AndroidUtilities.dp(30) / 2 + translateX, 0); + canvas.translate(containerWidth / 2, containerHeight / 2); + canvas.translate(containerWidth + AndroidUtilities.dp(30) / 2 + translateX, 0); canvas.scale(1.0f - scaleDiff, 1.0f - scaleDiff); int bitmapWidth = sideImage.getBitmapWidth(); int bitmapHeight = sideImage.getBitmapHeight(); - float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; - float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; + float scaleX = containerWidth / (float) bitmapWidth; + float scaleY = containerHeight / (float) bitmapHeight; float scale = Math.min(scaleX, scaleY); int width = (int) (bitmapWidth * scale); int height = (int) (bitmapHeight * scale); @@ -10750,7 +11964,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat canvas.save(); canvas.translate(translateX, currentTranslationY / currentScale); - canvas.translate((getContainerViewWidth() * (scale + 1) + AndroidUtilities.dp(30)) / 2, -currentTranslationY / currentScale); + canvas.translate((containerWidth * (scale + 1) + AndroidUtilities.dp(30)) / 2, -currentTranslationY / currentScale); photoProgressViews[1].setScale(1.0f - scaleDiff); photoProgressViews[1].setAlpha(alpha); photoProgressViews[1].onDraw(canvas); @@ -10761,15 +11975,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat float scaleDiff = 0; float alpha = 1; if (!zoomAnimation && translateX > maxX && currentEditMode == 0 && sendPhotoType != SELECT_TYPE_AVATAR) { - alpha = Math.min(1.0f, (translateX - maxX) / getContainerViewWidth()); + alpha = Math.min(1.0f, (translateX - maxX) / containerWidth); scaleDiff = alpha * 0.3f; alpha = 1.0f - alpha; translateX = maxX; } - boolean drawTextureView = aspectRatioFrameLayout != null && aspectRatioFrameLayout.getVisibility() == View.VISIBLE; + boolean drawTextureView = videoSizeSet && aspectRatioFrameLayout != null && aspectRatioFrameLayout.getVisibility() == View.VISIBLE; if (centerImage.hasBitmapImage() || drawTextureView && textureUploaded) { canvas.save(); - canvas.translate(getContainerViewWidth() / 2 + getAdditionX(), getContainerViewHeight() / 2 + getAdditionY()); + canvas.translate(containerWidth / 2 + getAdditionX(), containerHeight / 2 + getAdditionY()); canvas.translate(translateX, currentTranslationY + currentPanTranslationY); canvas.scale(currentScale - scaleDiff, currentScale - scaleDiff); if (currentEditMode == 3 && keyboardSize > AndroidUtilities.dp(20)) { @@ -10780,43 +11994,191 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - int bitmapWidth; - int bitmapHeight; - if (drawTextureView && textureUploaded) { - bitmapWidth = videoTextureView.getMeasuredWidth(); - bitmapHeight = videoTextureView.getMeasuredHeight(); - } else { - bitmapWidth = centerImage.getBitmapWidth(); - bitmapHeight = centerImage.getBitmapHeight(); - } - - float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; - float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; - float scale = Math.min(scaleX, scaleY); - int width = (int) (bitmapWidth * scale); - int height = (int) (bitmapHeight * scale); - - if (!drawTextureView || !textureUploaded || !videoCrossfadeStarted || videoCrossfadeAlpha != 1.0f) { + boolean drawCenterImage = false; + if (!pipAnimationInProgress && (!drawTextureView || !textureUploaded && !videoSizeSet || !videoCrossfadeStarted || videoCrossfadeAlpha != 1.0f)) { centerImage.setAlpha(alpha); + int width = centerImage.getBitmapWidth(); + int height = centerImage.getBitmapHeight(); + float scale; + if (isCurrentVideo && currentEditMode == 0 && sendPhotoType == SELECT_TYPE_AVATAR) { + scale = getCropFillScale(false); + } else { + scale = Math.min(containerWidth / (float) width, containerHeight / (float) height); + } + width *= scale; + height *= scale; centerImage.setImageCoords(-width / 2, -height / 2, width, height); - centerImage.draw(canvas); + if (isCurrentVideo) { + centerImage.draw(canvas); + } else { + drawCenterImage = true; + } } + + int bitmapWidth, originalWidth; + int bitmapHeight, originalHeight; + if (drawTextureView && textureUploaded && videoSizeSet) { + originalWidth = bitmapWidth = videoTextureView.getMeasuredWidth(); + originalHeight = bitmapHeight = videoTextureView.getMeasuredHeight(); + } else { + originalWidth = bitmapWidth = centerImage.getBitmapWidth(); + originalHeight = bitmapHeight = centerImage.getBitmapHeight(); + } + + float scale = Math.min(containerWidth / (float) originalWidth, containerHeight / (float) originalHeight); + int width = (int) (originalWidth * scale); + int height = (int) (originalHeight * scale); + + boolean applyCrop; + float scaleToFitX = 1.0f; + if (!imagesArrLocals.isEmpty()) { + if (currentEditMode == 3 || switchingToMode == 3) { + applyCrop = true; + } else if (sendPhotoType == SELECT_TYPE_AVATAR) { + applyCrop = (switchingToMode == 0 || currentEditMode != 3 && currentEditMode != 2); + } else { + applyCrop = imageMoveAnimation != null && switchingToMode != -1 || currentEditMode == 0 || currentEditMode == 1 || switchingToMode != -1; + } + } else { + applyCrop = false; + } + if (applyCrop) { + int rotatedWidth = originalWidth; + int rotatedHeight = originalHeight; + int orientation = cropTransform.getOrientation(); + if (orientation == 90 || orientation == 270) { + int temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + + temp = rotatedWidth; + rotatedWidth = rotatedHeight; + rotatedHeight = temp; + } + float cropAnimationValue; + if (sendPhotoType != SELECT_TYPE_AVATAR && (currentEditMode == 3 || switchingToMode == 3)) { + cropAnimationValue = 1.0f; + } else if (imageMoveAnimation != null && switchingToMode != -1) { + if (currentEditMode == 1 || switchingToMode == 1 || (currentEditMode == 2 || currentEditMode == 3) && switchingToMode == -1) { + cropAnimationValue = 1.0f; + } else if (switchingToMode == 0) { + cropAnimationValue = animationValue; + } else { + cropAnimationValue = 1.0f - animationValue; + } + } else { + cropAnimationValue = currentEditMode == 2 || currentEditMode == 3 ? 0.0f : 1.0f; + } + float cropPw = cropTransform.getCropPw(); + float cropPh = cropTransform.getCropPh(); + bitmapWidth *= cropPw + (1.0f - cropPw) * (1.0f - cropAnimationValue); + bitmapHeight *= cropPh + (1.0f - cropPh) * (1.0f - cropAnimationValue); + scaleToFitX = containerWidth / (float) bitmapWidth; + if (scaleToFitX * bitmapHeight > containerHeight) { + scaleToFitX = containerHeight / (float) bitmapHeight; + } + if (sendPhotoType != SELECT_TYPE_AVATAR && (currentEditMode != 1 || switchingToMode == 0) && editState.cropState != null) { + float startW = bitmapWidth * scaleToFitX; + float startH = bitmapHeight * scaleToFitX; + float originalScaleToFitX = containerWidth / (float) originalWidth; + if (originalScaleToFitX * originalHeight > containerHeight) { + originalScaleToFitX = containerHeight / (float) originalHeight; + } + float finalW = originalWidth * originalScaleToFitX / (currentScale - scaleDiff); + float finalH = originalHeight * originalScaleToFitX / (currentScale - scaleDiff); + + float w = startW + (finalW - startW) * (1.0f - cropAnimationValue); + float h = startH + (finalH - startH) * (1.0f - cropAnimationValue); + + canvas.clipRect(-w / 2, -h / 2, w / 2, h / 2); + } + if (sendPhotoType == SELECT_TYPE_AVATAR || cropTransform.hasViewTransform()) { + float cropScale; + if (currentEditMode == 1 || sendPhotoType == SELECT_TYPE_AVATAR) { + if (videoTextureView != null) { + videoTextureView.setScaleX(cropTransform.isMirrored() ? -1.0f : 1.0f); + } + float trueScale = 1.0f + (cropTransform.getTrueCropScale() - 1.0f) * (1.0f - cropAnimationValue); + cropScale = cropTransform.getScale() / trueScale; + float scaleToFit = containerWidth / (float) rotatedWidth; + if (scaleToFit * rotatedHeight > containerHeight) { + scaleToFit = containerHeight / (float) rotatedHeight; + } + cropScale *= scaleToFit / scale; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + if (currentEditMode == 3 || switchingToMode == 3) { + cropScale /= 1.0f + (cropTransform.getMinScale() - 1.0f) * (1.0f - cropAnimationValue); + } else if (switchingToMode == 0) { + cropScale /= cropTransform.getMinScale(); + } + } + } else { + if (videoTextureView != null) { + videoTextureView.setScaleX(editState.cropState != null && editState.cropState.mirrored ? -1.0f : 1.0f); + } + cropScale = editState.cropState != null ? editState.cropState.cropScale : 1.0f; + float trueScale = 1.0f + (cropScale - 1.0f) * (1.0f - cropAnimationValue); + cropScale *= scaleToFitX / scale / trueScale; + } + + canvas.translate(cropTransform.getCropAreaX() * cropAnimationValue, cropTransform.getCropAreaY() * cropAnimationValue); + canvas.scale(cropScale, cropScale); + canvas.translate(cropTransform.getCropPx() * rotatedWidth * scale * cropAnimationValue, cropTransform.getCropPy() * rotatedHeight * scale * cropAnimationValue); + float rotation = (cropTransform.getRotation() + orientation); + if (rotation > 180) { + rotation -= 360; + } + if (sendPhotoType == SELECT_TYPE_AVATAR && (currentEditMode == 3 || switchingToMode == 3)) { + canvas.rotate(rotation); + } else { + canvas.rotate(rotation * cropAnimationValue); + } + } else { + if (videoTextureView != null) { + videoTextureView.setScaleX(1.0f); + videoTextureView.setScaleY(1.0f); + } + } + } + if (currentEditMode == 3) { + photoPaintView.setTransform(currentScale, currentTranslationX, currentTranslationY, bitmapWidth * scaleToFitX, bitmapHeight * scaleToFitX); + } + + if (drawCenterImage) { + boolean mirror = false; + if (!imagesArrLocals.isEmpty()) { + if (currentEditMode == 1 || sendPhotoType == SELECT_TYPE_AVATAR) { + mirror = cropTransform.isMirrored(); + } else { + mirror = editState.cropState != null && editState.cropState.mirrored; + } + } + if (mirror) { + canvas.save(); + canvas.scale(-1, 1); + } + centerImage.draw(canvas); + if (mirror) { + canvas.restore(); + } + } + canvas.translate(-width / 2, -height / 2); if (drawTextureView || paintingOverlay.getVisibility() == View.VISIBLE) { - canvas.translate(-width / 2, -height / 2); canvas.scale(scale, scale); } if (drawTextureView) { - if (!videoCrossfadeStarted && textureUploaded) { + if (!videoCrossfadeStarted && textureUploaded && videoSizeSet) { videoCrossfadeStarted = true; videoCrossfadeAlpha = 0.0f; videoCrossfadeAlphaLastTime = System.currentTimeMillis(); containerView.getMeasuredHeight(); } videoTextureView.setAlpha(alpha * videoCrossfadeAlpha); - aspectRatioFrameLayout.draw(canvas); if (videoTextureView instanceof VideoEditTextureView) { - ((VideoEditTextureView) videoTextureView).setViewRect((getContainerViewWidth() - width) / 2 + getAdditionX() + translateX, (getContainerViewHeight() - height) / 2 + getAdditionY() + currentTranslationY + currentPanTranslationY, width, height); + VideoEditTextureView videoEditTextureView = (VideoEditTextureView) videoTextureView; + videoEditTextureView.setViewRect((containerWidth - width) / 2 + getAdditionX() + translateX, (containerHeight - height) / 2 + getAdditionY() + currentTranslationY + currentPanTranslationY, width, height); } + aspectRatioFrameLayout.draw(canvas); if (videoCrossfadeStarted && videoCrossfadeAlpha < 1.0f) { videoCrossfadeAlpha += dt / (playerInjected ? 100.0f : 200.0f); containerView.invalidate(); @@ -10824,9 +12186,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat videoCrossfadeAlpha = 1.0f; } } - paintingOverlay.setAlpha(alpha * videoCrossfadeAlpha); + paintingOverlay.setAlpha(alpha); } - if (paintingOverlay.getVisibility() == View.VISIBLE) { + if (paintingOverlay.getVisibility() == View.VISIBLE && (isCurrentVideo || currentEditMode != 2 || switchingToMode != -1)) { canvas.clipRect(0, 0, paintingOverlay.getMeasuredWidth(), paintingOverlay.getMeasuredHeight()); paintingOverlay.draw(canvas); } @@ -10862,7 +12224,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } boolean drawProgress; if (isCurrentVideo) { - drawProgress = videoPlayer == null || !videoPlayer.isPlaying(); + drawProgress = (videoTimelineView == null || !videoTimelineView.isDragging()) && (sendPhotoType != SELECT_TYPE_AVATAR || manuallyPaused) && (videoPlayer == null || !videoPlayer.isPlaying()); + if (drawProgress && containerView.getKeyboardHeight() > 0) { + drawProgress = captionEditText.getTop() > photoProgressViews[0].getY() + photoProgressViews[0].size; + } } else { drawProgress = true; } @@ -10896,13 +12261,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (sideImage == leftImage) { if (sideImage.hasBitmapImage()) { canvas.save(); - canvas.translate(getContainerViewWidth() / 2, getContainerViewHeight() / 2); - canvas.translate(-(getContainerViewWidth() * (scale + 1) + AndroidUtilities.dp(30)) / 2 + currentTranslationX, 0); + canvas.translate(containerWidth / 2, containerHeight / 2); + canvas.translate(-(containerWidth * (scale + 1) + AndroidUtilities.dp(30)) / 2 + currentTranslationX, 0); int bitmapWidth = sideImage.getBitmapWidth(); int bitmapHeight = sideImage.getBitmapHeight(); - float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; - float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; + float scaleX = containerWidth / (float) bitmapWidth; + float scaleY = containerHeight / (float) bitmapHeight; float scale = Math.min(scaleX, scaleY); int width = (int) (bitmapWidth * scale); int height = (int) (bitmapHeight * scale); @@ -10916,7 +12281,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat canvas.save(); canvas.translate(currentTranslationX, currentTranslationY / currentScale); - canvas.translate(-(getContainerViewWidth() * (scale + 1) + AndroidUtilities.dp(30)) / 2, -currentTranslationY / currentScale); + canvas.translate(-(containerWidth * (scale + 1) + AndroidUtilities.dp(30)) / 2, -currentTranslationY / currentScale); photoProgressViews[2].setScale(1.0f); photoProgressViews[2].setAlpha(1.0f); photoProgressViews[2].onDraw(canvas); @@ -11176,6 +12541,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { if (state == PROGRESS_PLAY || state == PROGRESS_PAUSE) { if (photoProgressViews[0].isVisible()) { + manuallyPaused = true; toggleVideoPlayer(); return true; } @@ -11187,9 +12553,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat toggleActionBar(!isActionBarVisible, true); } else if (sendPhotoType == 0 || sendPhotoType == 4) { if (isCurrentVideo) { - if (videoPlayer != null && !muteVideo) { + if (videoPlayer != null && !muteVideo && sendPhotoType != SELECT_TYPE_AVATAR) { videoPlayer.setVolume(1.0f); } + manuallyPaused = true; toggleVideoPlayer(); } else { checkImageView.performClick(); @@ -11207,6 +12574,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } else if (sendPhotoType == 2) { if (isCurrentVideo) { + manuallyPaused = true; toggleVideoPlayer(); } } @@ -11284,6 +12652,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return true; } + private boolean enableSwipeToPiP() { + if (!BuildVars.DEBUG_PRIVATE_VERSION) { + return false; + } + boolean permissionsEnabled = Build.VERSION.SDK_INT < 23 || Settings.canDrawOverlays(parentActivity); + return pipAvailable && textureUploaded && videoPlayer != null && videoPlayer.getRepeatCount() == 0 && permissionsEnabled && !(changingTextureView || switchingInlineMode || isInline); + } + @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; @@ -11294,8 +12670,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private PickerBottomLayoutViewer qualityPicker; private RadialProgressView progressView; private VideoTimelinePlayView videoTimelineView; + private TextView videoAvatarTooltip; private AnimatorSet qualityChooseViewAnimation; + private long captureFrameAtTime = -1; + private long captureFrameReadyAtTime = -1; + private long needCaptureFrameReadyAtTime = -1; + private int selectedCompression; private int compressionsCount = -1; private int previousCompression; @@ -11316,9 +12697,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private float videoCutEnd; private long audioFramesSize; private long videoFramesSize; - private int estimatedSize; + private long estimatedSize; private long estimatedDuration; private long originalSize; + private long avatarStartTime; + private float avatarStartProgress; private Runnable currentLoadingVideoRunnable; private MessageObject videoPreviewMessageObject; @@ -11436,6 +12819,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat muteItem.setEnabled(false); muteItem.setClickable(false); muteItem.animate().alpha(0.5f).setDuration(180).start(); + videoTimelineView.setMode(VideoTimelinePlayView.MODE_VIDEO); } else { muteItem.setEnabled(true); muteItem.setClickable(true); @@ -11447,7 +12831,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat compressItem.setAlpha(0.5f); compressItem.setEnabled(false); } - //videoTimelineView.setMaxProgressDiff(30000.0f / videoDuration); + if (sendPhotoType == SELECT_TYPE_AVATAR) { + videoTimelineView.setMaxProgressDiff(9600.0f / videoDuration); + videoTimelineView.setMode(VideoTimelinePlayView.MODE_AVATAR); + updateVideoInfo(); + } else { + videoTimelineView.setMaxProgressDiff(1.0f); + videoTimelineView.setMode(VideoTimelinePlayView.MODE_VIDEO); + } muteItem.setContentDescription(LocaleController.getString("NoSound", R.string.NoSound)); } else { actionBar.setSubtitle(currentSubtitle); @@ -11457,7 +12848,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat compressItem.setAlpha(1.0f); compressItem.setEnabled(true); } - //videoTimelineView.setMaxProgressDiff(1.0f); + videoTimelineView.setMaxProgressDiff(1.0f); + videoTimelineView.setMode(VideoTimelinePlayView.MODE_VIDEO); } } } @@ -11466,7 +12858,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat SharedPreferences preferences = MessagesController.getGlobalMainSettings(); SharedPreferences.Editor editor = preferences.edit(); editor.putInt("compress_video2", selectedCompression); - editor.apply(); + editor.commit(); updateWidthHeightBitrateForCompression(); updateVideoInfo(); if (request) { @@ -11486,33 +12878,44 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int compressIconWidth = 64; if (selectedCompression < 2) { compressItem.setImageResource(R.drawable.video_quality1); - compressIconWidth = 48; } else if (selectedCompression == 2) { compressItem.setImageResource(R.drawable.video_quality2); - compressIconWidth = 64; } else if (selectedCompression == 3) { compressItem.setImageResource(R.drawable.video_quality3); - compressIconWidth = 64; } - compressItem.setPadding(AndroidUtilities.dp(70 - compressIconWidth) / 2, 0, AndroidUtilities.dp(70 - compressIconWidth) / 2, 0); - compressItem.requestLayout(); - String[] compressionStrings = {"360", "480", "720", "1080"}; - compressItem.setContentDescription(LocaleController.getString("AccDescrVideoQuality", R.string.AccDescrVideoQuality) + ", " + compressionStrings[Math.max(0, selectedCompression)]); + itemsLayout.requestLayout(); estimatedDuration = (long) Math.ceil((videoTimelineView.getRightProgress() - videoTimelineView.getLeftProgress()) * videoDuration); int width; int height; - if (compressItem.getTag() == null) { + if (muteVideo) { + width = rotationValue == 90 || rotationValue == 270 ? resultHeight : resultWidth; + height = rotationValue == 90 || rotationValue == 270 ? resultWidth : resultHeight; + int bitrate; + if (sendPhotoType == SELECT_TYPE_AVATAR) { + if (estimatedDuration <= 2000) { + bitrate = 2600000; + } else if (estimatedDuration <= 5000) { + bitrate = 2200000; + } else { + bitrate = 1560000; + } + } else { + bitrate = 921600; + } + estimatedSize = (long) (bitrate / 8 * (estimatedDuration / 1000.0f)); + estimatedSize += estimatedSize / (32 * 1024) * 16; + } else if (compressItem.getTag() == null) { width = rotationValue == 90 || rotationValue == 270 ? originalHeight : originalWidth; height = rotationValue == 90 || rotationValue == 270 ? originalWidth : originalHeight; - estimatedSize = (int) (originalSize * ((float) estimatedDuration / videoDuration)); + estimatedSize = (long) (originalSize * ((float) estimatedDuration / videoDuration)); } else { width = rotationValue == 90 || rotationValue == 270 ? resultHeight : resultWidth; height = rotationValue == 90 || rotationValue == 270 ? resultWidth : resultHeight; - estimatedSize = (int) ((audioFramesSize + videoFramesSize) * ((float) estimatedDuration / videoDuration)); + estimatedSize = (long) (((sendPhotoType == SELECT_TYPE_AVATAR ? 0 : audioFramesSize) + videoFramesSize) * ((float) estimatedDuration / videoDuration)); estimatedSize += estimatedSize / (32 * 1024) * 16; } @@ -11549,7 +12952,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat photoProgressViews[0].setProgress(0, photoProgressViews[0].backgroundState == 0 || photoProgressViews[0].previousBackgroundState == 0); photoProgressViews[0].setBackgroundState(PROGRESS_PLAY, false); if (!wasRequestingPreview) { - preparePlayer(currentPlayingVideoFile, false, false, currentSavedFilterState); + preparePlayer(currentPlayingVideoFile, false, false, editState.savedFilterState); videoPlayer.seekTo((long) (videoTimelineView.getLeftProgress() * videoDuration)); } else { loadInitialVideo = true; @@ -11601,7 +13004,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat tryStartRequestPreviewOnFinish = false; photoProgressViews[0].setBackgroundState(PROGRESS_PLAY, false); if (request == 2) { - preparePlayer(currentPlayingVideoFile, false, false, currentSavedFilterState); + preparePlayer(currentPlayingVideoFile, false, false, editState.savedFilterState); videoPlayer.seekTo((long) (videoTimelineView.getLeftProgress() * videoDuration)); } } @@ -11615,32 +13018,42 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (selectedCompression >= compressionsCount) { selectedCompression = compressionsCount - 1; } - float maxSize; - switch (selectedCompression) { - case 0: - maxSize = 480.0f; - break; - case 1: - maxSize = 854.0f; - break; - case 2: - maxSize = 1280.0f; - break; - case 3: - default: - maxSize = 1920.0f; - break; - } - float scale = originalWidth > originalHeight ? maxSize / originalWidth : maxSize / originalHeight; - if (selectedCompression == compressionsCount - 1 && scale >= 1f) { - resultWidth = originalWidth; - resultHeight = originalHeight; - } else { + + if (sendPhotoType == SELECT_TYPE_AVATAR) { + float scale = Math.max(800.0f / originalWidth, 800.0f / originalHeight); resultWidth = Math.round(originalWidth * scale / 2) * 2; resultHeight = Math.round(originalHeight * scale / 2) * 2; + } else { + float maxSize; + switch (selectedCompression) { + case 0: + maxSize = 480.0f; + break; + case 1: + maxSize = 854.0f; + break; + case 2: + maxSize = 1280.0f; + break; + case 3: + default: + maxSize = 1920.0f; + break; + } + float scale = originalWidth > originalHeight ? maxSize / originalWidth : maxSize / originalHeight; + if (selectedCompression == compressionsCount - 1 && scale >= 1f) { + resultWidth = originalWidth; + resultHeight = originalHeight; + } else { + resultWidth = Math.round(originalWidth * scale / 2) * 2; + resultHeight = Math.round(originalHeight * scale / 2) * 2; + } } + if (bitrate != 0) { - if (resultWidth == originalWidth && resultHeight == originalHeight) { + if (sendPhotoType == SELECT_TYPE_AVATAR) { + bitrate = 1560000; + } else if (resultWidth == originalWidth && resultHeight == originalHeight) { bitrate = originalBitrate; } else { bitrate = MediaController.makeVideoBitrate(originalHeight, originalWidth, originalBitrate, resultHeight, resultWidth); @@ -11758,7 +13171,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } videoTimelineView.setVideoPath(videoPath, start, end); videoPreviewMessageObject = null; - muteVideo = muted; + muteVideo = muted || sendPhotoType == SELECT_TYPE_AVATAR; compressionsCount = -1; rotationValue = 0; @@ -11779,12 +13192,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (currentLoadingVideoRunnable != this) { return; } - currentLoadingVideoRunnable = null; + Runnable thisFinal = this; AndroidUtilities.runOnUIThread(() -> { - if (parentActivity == null) { + if (parentActivity == null || thisFinal != currentLoadingVideoRunnable) { return; } - + currentLoadingVideoRunnable = null; boolean hasAudio = params[AnimatedFileDrawable.PARAM_NUM_HAS_AUDIO] != 0; videoConvertSupported = params[AnimatedFileDrawable.PARAM_NUM_SUPPORTED_VIDEO_CODEC] != 0 && (!hasAudio || params[AnimatedFileDrawable.PARAM_NUM_SUPPORTED_AUDIO_CODEC] != 0); @@ -11800,8 +13213,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat resultHeight = originalHeight = params[AnimatedFileDrawable.PARAM_NUM_HEIGHT]; updateCompressionsCount(originalWidth, originalHeight); - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - selectedCompression = preferences.getInt("compress_video2", selectCompression()); + selectedCompression = selectCompression(); updateWidthHeightBitrateForCompression(); if (selectedCompression > compressionsCount - 1) { @@ -11810,44 +13222,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat setCompressItemEnabled(compressionsCount > 1, true); if (BuildVars.LOGS_ENABLED) { - FileLog.d("compressionsCount = " + compressionsCount + " w = " + originalWidth + " h = " + originalHeight); + FileLog.d("compressionsCount = " + compressionsCount + " w = " + originalWidth + " h = " + originalHeight + " r = " + rotationValue); } if (Build.VERSION.SDK_INT < 18 && compressItem.getTag() != null) { - try { - MediaCodecInfo codecInfo = MediaController.selectCodec(MediaController.VIDEO_MIME_TYPE); - if (codecInfo == null) { - if (BuildVars.LOGS_ENABLED) { - FileLog.d("no codec info for " + MediaController.VIDEO_MIME_TYPE); - } - videoConvertSupported = false; - setCompressItemEnabled(false, true); - } else { - String name = codecInfo.getName(); - if (name.equals("OMX.google.h264.encoder") || - name.equals("OMX.ST.VFM.H264Enc") || - name.equals("OMX.Exynos.avc.enc") || - name.equals("OMX.MARVELL.VIDEO.HW.CODA7542ENCODER") || - name.equals("OMX.MARVELL.VIDEO.H264ENCODER") || - name.equals("OMX.k3.video.encoder.avc") || - name.equals("OMX.TI.DUCATI1.VIDEO.H264E")) { - if (BuildVars.LOGS_ENABLED) { - FileLog.d("unsupported encoder = " + name); - } - videoConvertSupported = false; - setCompressItemEnabled(false, true); - } else { - if (MediaController.selectColorFormat(codecInfo, MediaController.VIDEO_MIME_TYPE) == 0) { - if (BuildVars.LOGS_ENABLED) { - FileLog.d("no color format for " + MediaController.VIDEO_MIME_TYPE); - } - setCompressItemEnabled(false, true); - } - } - } - } catch (Exception e) { - setCompressItemEnabled(false, true); - FileLog.e(e); - } + videoConvertSupported = false; + setCompressItemEnabled(false, true); } qualityChooseView.invalidate(); } else { @@ -11863,6 +13242,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } private int selectCompression() { + SharedPreferences preferences = MessagesController.getGlobalMainSettings(); + int compressionsCount = this.compressionsCount; + while (compressionsCount < 5) { + int selectedCompression = preferences.getInt(String.format(Locale.US, "compress_video_%d", compressionsCount), -1); + if (selectedCompression >= 0) { + return selectedCompression; + } + compressionsCount++; + } return Math.round(DownloadController.getInstance(currentAccount).getMaxVideoBitrate() / (100f / compressionsCount)) - 1; } @@ -11896,7 +13284,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat compressItemAnimation.playTogether( ObjectAnimator.ofFloat(compressItem, View.ALPHA, enabled ? 1.0f : 0.5f), ObjectAnimator.ofFloat(paintItem, View.ALPHA, videoConvertSupported ? 1.0f : 0.5f), - ObjectAnimator.ofFloat(tuneItem, View.ALPHA, videoConvertSupported ? 1.0f : 0.5f)); + ObjectAnimator.ofFloat(tuneItem, View.ALPHA, videoConvertSupported ? 1.0f : 0.5f), + ObjectAnimator.ofFloat(cropItem, View.ALPHA, videoConvertSupported ? 1.0f : 0.5f)); compressItemAnimation.setDuration(180); compressItemAnimation.setInterpolator(decelerateInterpolator); compressItemAnimation.start(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java index 320f776dc..dc0428093 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java @@ -116,6 +116,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC private boolean finished = false; private CharSequence lastPrintString; private MessageObject currentMessageObject = null; + private MessageObject[] setMessageObjects = new MessageObject[3]; private int currentMessageNum = 0; private PowerManager.WakeLock wakeLock = null; private boolean animationInProgress = false; @@ -1127,6 +1128,21 @@ public class PopupNotificationActivity extends Activity implements NotificationC } } } + for (int a = 0; a < 3; a++) { + int num = currentMessageNum - 1 + a; + MessageObject messageObject; + if (popupMessages.size() == 1 && (num < 0 || num >= popupMessages.size())) { + messageObject = null; + } else { + if (num == -1) { + num = popupMessages.size() - 1; + } else if (num == popupMessages.size()) { + num = 0; + } + messageObject = popupMessages.get(num); + } + setMessageObjects[a] = messageObject; + } } private void fixLayout() { @@ -1444,6 +1460,25 @@ public class PopupNotificationActivity extends Activity implements NotificationC } } getNewMessage(); + if (!popupMessages.isEmpty()) { + for (int a = 0; a < 3; a++) { + int num = currentMessageNum - 1 + a; + MessageObject messageObject; + if (popupMessages.size() == 1 && (num < 0 || num >= popupMessages.size())) { + messageObject = null; + } else { + if (num == -1) { + num = popupMessages.size() - 1; + } else if (num == popupMessages.size()) { + num = 0; + } + messageObject = popupMessages.get(num); + } + if (setMessageObjects[a] != messageObject) { + updateInterfaceForCurrentMessage(0); + } + } + } } } else if (id == NotificationCenter.updateInterfaces) { if (currentMessageObject == null || account != lastResumedAccount) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PrivacySettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PrivacySettingsActivity.java index f852ab532..9f463ff94 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PrivacySettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PrivacySettingsActivity.java @@ -73,8 +73,10 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio private int sessionsRow; private int passcodeRow; private int sessionsDetailRow; + private int newChatsHeaderRow; + private int newChatsRow; + private int newChatsSectionRow; private int advancedSectionRow; - private int clearDraftsRow; private int deleteAccountRow; private int deleteAccountDetailRow; private int botsSectionRow; @@ -97,6 +99,7 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio private boolean newSync; private boolean currentSuggest; private boolean newSuggest; + private boolean archiveChats; private boolean[] clear = new boolean[2]; @@ -108,6 +111,10 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio getMessagesController().getBlockedUsers(true); currentSync = newSync = getUserConfig().syncContacts; currentSuggest = newSuggest = getUserConfig().suggestContacts; + TLRPC.TL_globalPrivacySettings privacySettings = getContactsController().getGlobalPrivacySettings(); + if (privacySettings != null) { + archiveChats = privacySettings.archive_and_mute_new_noncontact_peers; + } updateRows(); loadPasswordSettings(); @@ -125,9 +132,10 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio getNotificationCenter().removeObserver(this, NotificationCenter.privacyRulesUpdated); getNotificationCenter().removeObserver(this, NotificationCenter.blockedUsersDidLoad); getNotificationCenter().removeObserver(this, NotificationCenter.didSetOrRemoveTwoStepPassword); + boolean save = false; if (currentSync != newSync) { getUserConfig().syncContacts = newSync; - getUserConfig().saveConfig(false); + save = true; if (newSync) { getContactsController().forceImportContacts(); if (getParentActivity() != null) { @@ -140,13 +148,28 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio getMediaDataController().clearTopPeers(); } getUserConfig().suggestContacts = newSuggest; - getUserConfig().saveConfig(false); + save = true; TLRPC.TL_contacts_toggleTopPeers req = new TLRPC.TL_contacts_toggleTopPeers(); req.enabled = newSuggest; getConnectionsManager().sendRequest(req, (response, error) -> { }); } + TLRPC.TL_globalPrivacySettings globalPrivacySettings = getContactsController().getGlobalPrivacySettings(); + if (globalPrivacySettings != null && globalPrivacySettings.archive_and_mute_new_noncontact_peers != archiveChats) { + globalPrivacySettings.archive_and_mute_new_noncontact_peers = archiveChats; + save = true; + TLRPC.TL_account_setGlobalPrivacySettings req = new TLRPC.TL_account_setGlobalPrivacySettings(); + req.settings = new TLRPC.TL_globalPrivacySettings(); + req.settings.flags |= 1; + req.settings.archive_and_mute_new_noncontact_peers = archiveChats; + getConnectionsManager().sendRequest(req, (response, error) -> { + + }); + } + if (save) { + getUserConfig().saveConfig(false); + } } @Override @@ -191,21 +214,6 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio presentFragment(new SessionsActivity(0)); } else if (position == webSessionsRow) { presentFragment(new SessionsActivity(1)); - } else if (position == clearDraftsRow) { - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setTitle(LocaleController.getString("AreYouSureClearDraftsTitle", R.string.AreYouSureClearDraftsTitle)); - builder.setMessage(LocaleController.getString("AreYouSureClearDrafts", R.string.AreYouSureClearDrafts)); - builder.setPositiveButton(LocaleController.getString("Delete", R.string.Delete), (dialogInterface, i) -> { - TLRPC.TL_messages_clearAllDrafts req = new TLRPC.TL_messages_clearAllDrafts(); - getConnectionsManager().sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> getMediaDataController().clearAllDrafts(true))); - }); - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - AlertDialog alertDialog = builder.create(); - showDialog(alertDialog); - TextView button = (TextView) alertDialog.getButton(DialogInterface.BUTTON_POSITIVE); - if (button != null) { - button.setTextColor(Theme.getColor(Theme.key_dialogTextRed2)); - } } else if (position == deleteAccountRow) { if (getParentActivity() == null) { return; @@ -377,6 +385,10 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio newSuggest = !newSuggest; cell.setChecked(newSuggest); } + } else if (position == newChatsRow) { + final TextCheckCell cell = (TextCheckCell) view; + archiveChats = !archiveChats; + cell.setChecked(archiveChats); } else if (position == contactsSyncRow) { newSync = !newSync; if (view instanceof TextCheckCell) { @@ -465,6 +477,10 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio @Override public void didReceivedNotification(int id, int account, Object... args) { if (id == NotificationCenter.privacyRulesUpdated) { + TLRPC.TL_globalPrivacySettings privacySettings = getContactsController().getGlobalPrivacySettings(); + if (privacySettings != null) { + archiveChats = privacySettings.archive_and_mute_new_noncontact_peers; + } if (listAdapter != null) { listAdapter.notifyDataSetChanged(); } @@ -498,8 +514,16 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio passwordRow = rowCount++; sessionsRow = rowCount++; sessionsDetailRow = rowCount++; + if (getMessagesController().autoarchiveAvailable) { + newChatsHeaderRow = rowCount++; + newChatsRow = rowCount++; + newChatsSectionRow = rowCount++; + } else { + newChatsHeaderRow = -1; + newChatsRow = -1; + newChatsSectionRow = -1; + } advancedSectionRow = rowCount++; - clearDraftsRow = rowCount++; deleteAccountRow = rowCount++; deleteAccountDetailRow = rowCount++; botsSectionRow = rowCount++; @@ -672,7 +696,7 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { int position = holder.getAdapterPosition(); - return position == passcodeRow || position == passwordRow || position == blockedRow || position == sessionsRow || position == secretWebpageRow || position == webSessionsRow || position == clearDraftsRow || + return position == passcodeRow || position == passwordRow || position == blockedRow || position == sessionsRow || position == secretWebpageRow || position == webSessionsRow || position == groupsRow && !getContactsController().getLoadingPrivicyInfo(ContactsController.PRIVACY_RULES_TYPE_INVITE) || position == lastSeenRow && !getContactsController().getLoadingPrivicyInfo(ContactsController.PRIVACY_RULES_TYPE_LASTSEEN) || position == callsRow && !getContactsController().getLoadingPrivicyInfo(ContactsController.PRIVACY_RULES_TYPE_CALLS) || @@ -680,6 +704,7 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio position == forwardsRow && !getContactsController().getLoadingPrivicyInfo(ContactsController.PRIVACY_RULES_TYPE_FORWARDS) || position == phoneNumberRow && !getContactsController().getLoadingPrivicyInfo(ContactsController.PRIVACY_RULES_TYPE_PHONE) || position == deleteAccountRow && !getContactsController().getLoadingDeleteInfo() || + position == newChatsRow && !getContactsController().getLoadingGlobalSettings() || position == paymentsClearRow || position == secretMapRow || position == contactsSyncRow || position == passportRow || position == contactsDeleteRow || position == contactsSuggestRow; } @@ -806,9 +831,7 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio value = LocaleController.formatPluralString("Days", ttl); } } - textCell.setTextAndValue(LocaleController.getString("DeleteAccountIfAwayFor2", R.string.DeleteAccountIfAwayFor2), value, false); - } else if (position == clearDraftsRow) { - textCell.setText(LocaleController.getString("PrivacyDeleteCloudDrafts", R.string.PrivacyDeleteCloudDrafts), true); + textCell.setTextAndValue(LocaleController.getString("DeleteAccountIfAwayFor3", R.string.DeleteAccountIfAwayFor3), value, false); } else if (position == paymentsClearRow) { textCell.setText(LocaleController.getString("PrivacyPaymentsClear", R.string.PrivacyPaymentsClear), true); } else if (position == secretMapRow) { @@ -858,6 +881,9 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio }*/ privacyCell.setText(LocaleController.getString("SuggestContactsInfo", R.string.SuggestContactsInfo)); privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); + } else if (position == newChatsSectionRow) { + privacyCell.setText(LocaleController.getString("ArchiveAndMuteInfo", R.string.ArchiveAndMuteInfo)); + privacyCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); } break; case 2: @@ -867,13 +893,15 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio } else if (position == securitySectionRow) { headerCell.setText(LocaleController.getString("SecurityTitle", R.string.SecurityTitle)); } else if (position == advancedSectionRow) { - headerCell.setText(LocaleController.getString("PrivacyAdvanced", R.string.PrivacyAdvanced)); + headerCell.setText(LocaleController.getString("DeleteMyAccount", R.string.DeleteMyAccount)); } else if (position == secretSectionRow) { headerCell.setText(LocaleController.getString("SecretChat", R.string.SecretChat)); } else if (position == botsSectionRow) { headerCell.setText(LocaleController.getString("PrivacyBots", R.string.PrivacyBots)); } else if (position == contactsSectionRow) { headerCell.setText(LocaleController.getString("Contacts", R.string.Contacts)); + } else if (position == newChatsHeaderRow) { + headerCell.setText(LocaleController.getString("NewChatsFromNonContacts", R.string.NewChatsFromNonContacts)); } break; case 3: @@ -884,6 +912,8 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio textCheckCell.setTextAndCheck(LocaleController.getString("SyncContacts", R.string.SyncContacts), newSync, true); } else if (position == contactsSuggestRow) { textCheckCell.setTextAndCheck(LocaleController.getString("SuggestContacts", R.string.SuggestContacts), newSuggest, false); + } else if (position == newChatsRow) { + textCheckCell.setTextAndCheck(LocaleController.getString("ArchiveAndMute", R.string.ArchiveAndMute), archiveChats, false); } break; } @@ -891,13 +921,13 @@ public class PrivacySettingsActivity extends BaseFragment implements Notificatio @Override public int getItemViewType(int position) { - if (position == passportRow || position == lastSeenRow || position == phoneNumberRow || position == blockedRow || position == deleteAccountRow || position == sessionsRow || position == webSessionsRow || position == passwordRow || position == passcodeRow || position == groupsRow || position == paymentsClearRow || position == secretMapRow || position == contactsDeleteRow || position == clearDraftsRow) { + if (position == passportRow || position == lastSeenRow || position == phoneNumberRow || position == blockedRow || position == deleteAccountRow || position == sessionsRow || position == webSessionsRow || position == passwordRow || position == passcodeRow || position == groupsRow || position == paymentsClearRow || position == secretMapRow || position == contactsDeleteRow) { return 0; - } else if (position == deleteAccountDetailRow || position == groupsDetailRow || position == sessionsDetailRow || position == secretDetailRow || position == botsDetailRow || position == contactsDetailRow) { + } else if (position == deleteAccountDetailRow || position == groupsDetailRow || position == sessionsDetailRow || position == secretDetailRow || position == botsDetailRow || position == contactsDetailRow || position == newChatsSectionRow) { return 1; - } else if (position == securitySectionRow || position == advancedSectionRow || position == privacySectionRow || position == secretSectionRow || position == botsSectionRow || position == contactsSectionRow) { + } else if (position == securitySectionRow || position == advancedSectionRow || position == privacySectionRow || position == secretSectionRow || position == botsSectionRow || position == contactsSectionRow || position == newChatsHeaderRow) { return 2; - } else if (position == secretWebpageRow || position == contactsSyncRow || position == contactsSuggestRow) { + } else if (position == secretWebpageRow || position == contactsSyncRow || position == contactsSuggestRow || position == newChatsRow) { return 3; } return 0; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java index 71d0004c9..eab88cc1f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.DataSetObserver; @@ -41,8 +42,11 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.SystemClock; +import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; import android.util.Property; import android.util.SparseArray; import android.util.TypedValue; @@ -54,8 +58,10 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; +import android.view.WindowManager; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; @@ -67,6 +73,7 @@ import androidx.core.graphics.ColorUtils; import androidx.core.view.NestedScrollingParent3; import androidx.core.view.NestedScrollingParentHelper; import androidx.core.view.ViewCompat; +import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.PagerAdapter; @@ -75,10 +82,13 @@ import androidx.viewpager.widget.ViewPager; import org.telegram.PhoneFormat.PhoneFormat; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.BuildVars; import org.telegram.messenger.ChatObject; import org.telegram.messenger.ContactsController; +import org.telegram.messenger.Emoji; import org.telegram.messenger.FileLoader; import org.telegram.messenger.FileLog; +import org.telegram.messenger.ImageLoader; import org.telegram.messenger.ImageLocation; import org.telegram.messenger.ImageReceiver; import org.telegram.messenger.LocaleController; @@ -86,16 +96,17 @@ import org.telegram.messenger.MediaController; import org.telegram.messenger.MediaDataController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; -import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.NotificationsController; import org.telegram.messenger.R; -import org.telegram.messenger.SecretChatHelper; -import org.telegram.messenger.SendMessagesHelper; +import org.telegram.messenger.BuildConfig; import org.telegram.messenger.SharedConfig; import org.telegram.messenger.UserConfig; import org.telegram.messenger.UserObject; +import org.telegram.messenger.Utilities; +import org.telegram.messenger.browser.Browser; import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.SerializedData; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.ActionBar; @@ -109,23 +120,30 @@ import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ActionBar.ThemeDescription; import org.telegram.ui.Cells.AboutLinkCell; import org.telegram.ui.Cells.DividerCell; +import org.telegram.ui.Cells.GraySectionCell; import org.telegram.ui.Cells.HeaderCell; import org.telegram.ui.Cells.NotificationsCheckCell; +import org.telegram.ui.Cells.SettingsSearchCell; import org.telegram.ui.Cells.ShadowSectionCell; import org.telegram.ui.Cells.TextCell; import org.telegram.ui.Cells.TextDetailCell; import org.telegram.ui.Cells.TextInfoPrivacyCell; import org.telegram.ui.Cells.UserCell; import org.telegram.ui.Components.AlertsCreator; +import org.telegram.ui.Components.AnimatedFileDrawable; import org.telegram.ui.Components.AnimationProperties; import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.BackupImageView; +import org.telegram.ui.Components.ChatGreetingsView; import org.telegram.ui.Components.CombinedDrawable; import org.telegram.ui.Components.CrossfadeDrawable; import org.telegram.ui.Components.CubicBezierInterpolator; +import org.telegram.ui.Components.EmptyTextProgressView; import org.telegram.ui.Components.IdenticonDrawable; +import org.telegram.ui.Components.ImageUpdater; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.ProfileGalleryView; +import org.telegram.ui.Components.RadialProgressView; import org.telegram.ui.Components.RecyclerListView; import org.telegram.ui.Components.ScamDrawable; import org.telegram.ui.Components.SharedMediaLayout; @@ -137,17 +155,38 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Random; +import java.util.Set; import java.util.concurrent.CountDownLatch; +import cn.hutool.core.util.RuntimeUtil; +import kotlin.Unit; +import libv2ray.Libv2ray; +import tw.nekomimi.nekogram.BottomBuilder; import tw.nekomimi.nekogram.NekoConfig; +import tw.nekomimi.nekogram.NekoXConfig; +import tw.nekomimi.nekogram.NekoXSettingActivity; +import tw.nekomimi.nekogram.parts.UpdateChecksKt; +import tw.nekomimi.nekogram.settings.NekoSettingsActivity; +import tw.nekomimi.nekogram.utils.AlertUtil; +import tw.nekomimi.nekogram.utils.EnvUtil; +import tw.nekomimi.nekogram.utils.FileUtil; +import tw.nekomimi.nekogram.utils.IoUtil; +import tw.nekomimi.nekogram.utils.LangsKt; import tw.nekomimi.nekogram.utils.ProxyUtil; +import tw.nekomimi.nekogram.utils.ShareUtil; +import tw.nekomimi.nekogram.utils.ThreadUtil; +import tw.nekomimi.nekogram.utils.UIUtil; -public class ProfileActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate, DialogsActivity.DialogsActivityDelegate, SharedMediaLayout.SharedMediaPreloaderDelegate { +public class ProfileActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate, DialogsActivity.DialogsActivityDelegate, SharedMediaLayout.SharedMediaPreloaderDelegate, ImageUpdater.ImageUpdaterDelegate { private RecyclerListView listView; + private RecyclerListView searchListView; private LinearLayoutManager layoutManager; private ListAdapter listAdapter; - private AvatarImageView avatarImage; + private SearchAdapter searchAdapter; private SimpleTextView[] nameTextView = new SimpleTextView[2]; private SimpleTextView[] onlineTextView = new SimpleTextView[3]; private SimpleTextView idTextView; @@ -159,12 +198,21 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private CrossfadeDrawable verifiedCrossfadeDrawable; private ScamDrawable scamDrawable; private UndoView undoView; - private ProfileGalleryView avatarsViewPager; - private PagerIndicatorView avatarsViewPagerIndicatorView; private OverlaysView overlaysView; private SharedMediaLayout sharedMediaLayout; + private EmptyTextProgressView emptyView; private boolean sharedMediaLayoutAttached; private SharedMediaLayout.SharedMediaPreloader sharedMediaPreloader; + + private FrameLayout avatarContainer; + private AvatarImageView avatarImage; + private View avatarOverlay; + private AnimatorSet avatarAnimation; + private RadialProgressView avatarProgressView; + private ProfileGalleryView avatarsViewPager; + private PagerIndicatorView avatarsViewPagerIndicatorView; + private AvatarDrawable avatarDrawable; + private ImageUpdater imageUpdater; private int avatarColor; private int overlayCountVisible; @@ -174,15 +222,17 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private int listContentHeight; private boolean openingAvatar; + private boolean doNotSetForeground; + private boolean[] isOnline = new boolean[1]; private boolean callItemVisible; private boolean editItemVisible; - private AvatarDrawable avatarDrawable; private ActionBarMenuItem animatingItem; private ActionBarMenuItem callItem; private ActionBarMenuItem editItem; private ActionBarMenuItem otherItem; + private ActionBarMenuItem searchItem; protected float headerShadowAlpha = 1.0f; private TopView topView; private int user_id; @@ -195,6 +245,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private boolean expandPhoto; private boolean needSendMessage; + private boolean scrolling; + private boolean canSearchMembers; private boolean loadingUsers; @@ -203,6 +255,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private int banFromGroup; private boolean openAnimationInProgress; + private boolean transitionAnimationInProress; private boolean recreateMenuAfterAnimation; private int playProfileAnimation; private boolean allowProfileAnimation = true; @@ -210,6 +263,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private float initialAnimationExtraHeight; private float animationProgress; + private int searchTransitionOffset; + private float searchTransitionProgress; + private Animator searchViewTransition; + private boolean searchMode; + private HashMap positionToOffset = new HashMap<>(); private float avatarX; @@ -235,6 +293,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private TLRPC.ChatFull chatInfo; private TLRPC.UserFull userInfo; + private String currentBio; + private int selectedUser; private int onlineCount = -1; private ArrayList sortedUsers; @@ -244,6 +304,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private TLRPC.BotInfo botInfo; private TLRPC.ChannelParticipant currentChannelParticipant; + private TLRPC.FileLocation avatar; + private TLRPC.FileLocation avatarBig; + private final static int add_contact = 1; private final static int block_contact = 2; private final static int share_contact = 3; @@ -263,10 +326,47 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private final static int gallery_menu_save = 21; private final static int event_log = 22; + private final static int edit_name = 30; + private final static int logout = 31; + private final static int search_button = 32; + private final static int set_as_main = 33; + private final static int edit_avatar = 34; + private final static int delete_avatar = 35; + private final static int add_photo = 36; + private Rect rect = new Rect(); private int rowCount; + private int setAvatarRow; + private int setAvatarSectionRow; + private int numberSectionRow; + private int numberRow; + private int setUsernameRow; + private int bioRow; + private int settingsSectionRow; + private int settingsSectionRow2; + private int notificationRow; + private int nekoRow; + private int languageRow; + private int privacyRow; + private int dataRow; + private int chatRow; + private int stickersRow; + private int filtersRow; + private int devicesRow; + private int devicesSectionRow; + private int helpHeaderRow; + private int questionRow; + private int faqRow; + private int policyRow; + private int helpSectionCell; + private int debugHeaderRow; + private int sendLogsRow; + private int clearLogsRow; + private int switchBackendRow; + private int versionRow; + private int emptyRow; private int bottomPaddingRow; private int infoHeaderRow; @@ -283,7 +383,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private int settingsTimerRow; private int settingsKeyRow; - private int settingsSectionRow; + private int secretSettingsSectionRow; private int membersHeaderRow; private int membersStartRow; @@ -301,6 +401,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private int lastSectionRow; private int transitionIndex; + private TLRPC.Document preloadedSticker; private final Property HEADER_SHADOW = new AnimationProperties.FloatProperty("headerShadow") { @Override @@ -315,7 +416,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } }; - private PhotoViewer.PhotoViewerProvider provider = new PhotoViewer.EmptyPhotoViewerProvider() { + public PhotoViewer.PhotoViewerProvider provider = new PhotoViewer.EmptyPhotoViewerProvider() { @Override public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index, boolean needPreview) { @@ -325,12 +426,12 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. TLRPC.FileLocation photoBig = null; if (user_id != 0) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + TLRPC.User user = getMessagesController().getUser(user_id); if (user != null && user.photo != null && user.photo.photo_big != null) { photoBig = user.photo.photo_big; } } else if (chat_id != 0) { - TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(chat_id); + TLRPC.Chat chat = getMessagesController().getChat(chat_id); if (chat != null && chat.photo != null && chat.photo.photo_big != null) { photoBig = chat.photo.photo_big; } @@ -352,7 +453,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. object.thumb = object.imageReceiver.getBitmapSafe(); object.size = -1; object.radius = avatarImage.getImageReceiver().getRoundRadius(); - object.scale = avatarImage.getScaleX(); + object.scale = avatarContainer.getScaleX(); + object.canEdit = user_id == getUserConfig().clientUserId; return object; } return null; @@ -362,15 +464,21 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public void willHidePhotoViewer() { avatarImage.getImageReceiver().setVisible(true, true); } + + @Override + public void openPhotoForEdit(String file, String thumb, boolean isVideo) { + imageUpdater.openPhotoForEdit(file, thumb, 0, isVideo); + } }; - public static class AvatarImageView extends BackupImageView { + public class AvatarImageView extends BackupImageView { private final RectF rect = new RectF(); private final Paint placeholderPaint; private ImageReceiver foregroundImageReceiver; private float foregroundAlpha; + private ImageReceiver.BitmapHolder drawableHolder; public AvatarImageView(Context context) { super(context); @@ -381,10 +489,21 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public void setForegroundImage(ImageLocation imageLocation, String imageFilter, Drawable thumb) { foregroundImageReceiver.setImage(imageLocation, imageFilter, thumb, 0, null, null, 0); + if (drawableHolder != null) { + drawableHolder.release(); + drawableHolder = null; + } } - public void setForegroundImageDrawable(Drawable drawable) { - foregroundImageReceiver.setImageBitmap(drawable); + public void setForegroundImageDrawable(ImageReceiver.BitmapHolder holder) { + if (holder != null) { + foregroundImageReceiver.setImageBitmap(holder.drawable); + } + if (drawableHolder != null) { + drawableHolder.release(); + drawableHolder = null; + } + drawableHolder = holder; } public float getForegroundAlpha() { @@ -397,7 +516,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } public void clearForeground() { + AnimatedFileDrawable drawable = foregroundImageReceiver.getAnimation(); + if (drawable != null) { + drawable.removeSecondParentView(avatarImage); + } foregroundImageReceiver.clearImage(); + if (drawableHolder != null) { + drawableHolder.release(); + drawableHolder = null; + } foregroundAlpha = 0f; invalidate(); } @@ -405,6 +532,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. protected void onDetachedFromWindow() { super.onDetachedFromWindow(); foregroundImageReceiver.onDetachedFromWindow(); + if (drawableHolder != null) { + drawableHolder.release(); + drawableHolder = null; + } } @Override @@ -438,6 +569,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } } + + @Override + public void invalidate() { + super.invalidate(); + if (avatarsViewPager != null) { + BackupImageView imageView = avatarsViewPager.getCurrentItemView(); + avatarsViewPager.invalidate(); + } + } } private class TopView extends View { @@ -466,7 +606,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override protected void onDraw(Canvas canvas) { final int height = ActionBar.getCurrentActionBarHeight() + (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0); - final float v = extraHeight + height; + final float v = extraHeight + height + searchTransitionOffset; int y1 = (int) (v * (1.0f - mediaHeaderAnimationProgress)); @@ -488,7 +628,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private class OverlaysView extends View implements ProfileGalleryView.Callback { - private final int statusBarHeight = actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0; + private final int statusBarHeight = actionBar.getOccupyStatusBar() && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0; private final Rect topOverlayRect = new Rect(); private final Rect bottomOverlayRect = new Rect(); @@ -511,6 +651,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private float alpha = 1.0f; private float[] alphas = null; private long lastTime; + private float previousSelectedProgress; + private int previousSelectedPotision = -1; + private float currentProgress; + private int selectedPosition; + + private float currentLoadingAnimationProgress; + private int currentLoadingAnimationDirection = 1; public OverlaysView(Context context) { super(context); @@ -558,6 +705,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. }); } + public void saveCurrentPageProgress() { + previousSelectedProgress = currentProgress; + previousSelectedPotision = selectedPosition; + currentLoadingAnimationProgress = 0.0f; + currentLoadingAnimationDirection = 1; + } + public void setAlphaValue(float value, boolean self) { if (Build.VERSION.SDK_INT > 18) { int alpha = (int) (255 * value); @@ -628,7 +782,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. canvas.drawRect(bottomOverlayRect, backgroundPaint); int count = avatarsViewPager.getRealCount(); - int selected = avatarsViewPager.getRealPosition(); + selectedPosition = avatarsViewPager.getRealPosition(); if (alphas == null || alphas.length != count) { alphas = new float[count]; @@ -657,20 +811,56 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. selectedBarPaint.setAlpha((int) (0xff * alpha)); } int width = (getMeasuredWidth() - AndroidUtilities.dp(5 * 2) - AndroidUtilities.dp(2 * (count - 1))) / count; - int y = AndroidUtilities.dp(4) + (Build.VERSION.SDK_INT >= 21 ? AndroidUtilities.statusBarHeight : 0); + int y = AndroidUtilities.dp(4) + (Build.VERSION.SDK_INT >= 21 && !inBubbleMode ? AndroidUtilities.statusBarHeight : 0); for (int a = 0; a < count; a++) { int x = AndroidUtilities.dp(5 + a * 2) + width * a; - rect.set(x, y, x + width, y + AndroidUtilities.dp(2)); + float progress; + int baseAlpha = 0x55; + if (a == previousSelectedPotision && Math.abs(previousSelectedProgress - 1.0f) > 0.0001f) { + progress = previousSelectedProgress; + canvas.save(); + canvas.clipRect(x + width * progress, y, x + width, y + AndroidUtilities.dp(2)); + rect.set(x, y, x + width, y + AndroidUtilities.dp(2)); + barPaint.setAlpha((int) (0x55 * alpha)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(1), AndroidUtilities.dp(1), barPaint); + baseAlpha = 0x50; + canvas.restore(); + invalidate = true; + } else if (a == selectedPosition) { + if (avatarsViewPager.isCurrentItemVideo()) { + progress = currentProgress = avatarsViewPager.getCurrentItemProgress(); + if (progress <= 0 && avatarsViewPager.isLoadingCurrentVideo() || currentLoadingAnimationProgress > 0.0f) { + currentLoadingAnimationProgress += currentLoadingAnimationDirection * dt / 500.0f; + if (currentLoadingAnimationProgress > 1.0f) { + currentLoadingAnimationProgress = 1.0f; + currentLoadingAnimationDirection *= -1; + } else if (currentLoadingAnimationProgress <= 0) { + currentLoadingAnimationProgress = 0.0f; + currentLoadingAnimationDirection *= -1; + } + } + rect.set(x, y, x + width, y + AndroidUtilities.dp(2)); + barPaint.setAlpha((int) ((0x55 + 0x30 * currentLoadingAnimationProgress) * alpha)); + canvas.drawRoundRect(rect, AndroidUtilities.dp(1), AndroidUtilities.dp(1), barPaint); + invalidate = true; + baseAlpha = 0x50; + } else { + progress = currentProgress = 1.0f; + } + } else { + progress = 1.0f; + } + rect.set(x, y, x + width * progress, y + AndroidUtilities.dp(2)); - if (a != selected) { + if (a != selectedPosition) { if (overlayCountVisible == 3) { - barPaint.setAlpha((int) (AndroidUtilities.lerp(0x55, 0xff, CubicBezierInterpolator.EASE_BOTH.getInterpolation(alphas[a])) * alpha)); + barPaint.setAlpha((int) (AndroidUtilities.lerp(baseAlpha, 0xff, CubicBezierInterpolator.EASE_BOTH.getInterpolation(alphas[a])) * alpha)); } } else { alphas[a] = 0.75f; } - canvas.drawRoundRect(rect, AndroidUtilities.dp(1), AndroidUtilities.dp(1), a == selected ? selectedBarPaint : barPaint); + canvas.drawRoundRect(rect, AndroidUtilities.dp(1), AndroidUtilities.dp(1), a == selectedPosition ? selectedBarPaint : barPaint); } if (overlayCountVisible == 2) { @@ -685,12 +875,17 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } else if (overlayCountVisible == 3) { for (int i = 0; i < alphas.length; i++) { - if (i != selected && alphas[i] > 0.0f) { + if (i != selectedPosition && alphas[i] > 0.0f) { alphas[i] -= dt / 500.0f; - if (alphas[i] < 0.0f) { + if (alphas[i] <= 0.0f) { alphas[i] = 0.0f; + if (i == previousSelectedPotision) { + previousSelectedPotision = -1; + } } invalidate = true; + } else if (i == previousSelectedPotision) { + previousSelectedPotision = -1; } } } @@ -732,6 +927,16 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. Arrays.fill(pressedOverlayVisible, false); postInvalidateOnAnimation(); } + + @Override + public void onPhotosLoaded() { + updateProfileData(); + } + + @Override + public void onVideoSet() { + invalidate(); + } } private class NestedFrameLayout extends FrameLayout implements NestedScrollingParent3 { @@ -892,13 +1097,19 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } }); avatarsViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + + private int prevPage; + @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { - invalidateIndicatorRect(); + int realPosition = avatarsViewPager.getRealPosition(position); + invalidateIndicatorRect(prevPage != realPosition); + prevPage = realPosition; + updateAvatarItems(); } @Override @@ -912,12 +1123,40 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (overlayCountVisible == 0 && count > 1 && count <= 20 && overlaysView.isOverlaysVisible()) { overlayCountVisible = 1; } - invalidateIndicatorRect(); + invalidateIndicatorRect(false); refreshVisibility(1f); + updateAvatarItems(); } }); } + private void updateAvatarItemsInternal() { + if (otherItem == null || avatarsViewPager == null) { + return; + } + if (isPulledDown) { + int position = avatarsViewPager.getRealPosition(); + if (position == 0) { + otherItem.hideSubItem(set_as_main); + otherItem.showSubItem(add_photo); + } else { + otherItem.showSubItem(set_as_main); + otherItem.hideSubItem(add_photo); + } + } + } + + private void updateAvatarItems() { + if (imageUpdater == null) { + return; + } + if (otherItem.isSubMenuShowing()) { + AndroidUtilities.runOnUIThread(this::updateAvatarItemsInternal, 500); + } else { + updateAvatarItemsInternal(); + } + } + public boolean isIndicatorVisible() { return isIndicatorVisible; } @@ -950,10 +1189,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { - invalidateIndicatorRect(); + invalidateIndicatorRect(false); } - private void invalidateIndicatorRect() { + private void invalidateIndicatorRect(boolean pageChanged) { + if (pageChanged) { + overlaysView.saveCurrentPageProgress(); + } overlaysView.invalidate(); final float textWidth = textPaint.measureText(getCurrentTitle()); indicatorRect.right = getMeasuredWidth() - AndroidUtilities.dp(54f); @@ -981,6 +1223,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return callItem; } else if (editItemVisible) { return editItem; + } else if (searchItem != null) { + return searchItem; } else { return null; } @@ -1011,33 +1255,43 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (user_id != 0) { dialog_id = arguments.getLong("dialog_id", 0); if (dialog_id != 0) { - currentEncryptedChat = MessagesController.getInstance(currentAccount).getEncryptedChat((int) (dialog_id >> 32)); + currentEncryptedChat = getMessagesController().getEncryptedChat((int) (dialog_id >> 32)); } - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + TLRPC.User user = getMessagesController().getUser(user_id); if (user == null) { return false; } - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.updateInterfaces); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.contactsDidLoad); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.encryptedChatCreated); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.encryptedChatUpdated); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.blockedUsersDidLoad); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.botInfoDidLoad); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.userInfoDidLoad); - userBlocked = MessagesController.getInstance(currentAccount).blockedUsers.indexOfKey(user_id) >= 0; + + getNotificationCenter().addObserver(this, NotificationCenter.contactsDidLoad); + getNotificationCenter().addObserver(this, NotificationCenter.encryptedChatCreated); + getNotificationCenter().addObserver(this, NotificationCenter.encryptedChatUpdated); + getNotificationCenter().addObserver(this, NotificationCenter.blockedUsersDidLoad); + getNotificationCenter().addObserver(this, NotificationCenter.botInfoDidLoad); + getNotificationCenter().addObserver(this, NotificationCenter.userInfoDidLoad); + userBlocked = getMessagesController().blockedUsers.indexOfKey(user_id) >= 0; if (user.bot) { isBot = true; - MediaDataController.getInstance(currentAccount).loadBotInfo(user.id, true, classGuid); + getMediaDataController().loadBotInfo(user.id, true, classGuid); } - userInfo = MessagesController.getInstance(currentAccount).getUserFull(user_id); - MessagesController.getInstance(currentAccount).loadFullUser(MessagesController.getInstance(currentAccount).getUser(user_id), classGuid, true); + userInfo = getMessagesController().getUserFull(user_id); + getMessagesController().loadFullUser(getMessagesController().getUser(user_id), classGuid, true); participantsMap = null; + + if (UserObject.isUserSelf(user)) { + imageUpdater = new ImageUpdater(true); + imageUpdater.setOpenWithFrontfaceCamera(true); + imageUpdater.parentFragment = this; + imageUpdater.setDelegate(this); + getMediaDataController().checkFeaturedStickers(); + getMessagesController().loadSuggestedFilters(); + getMessagesController().loadUserInfo(getUserConfig().getCurrentUser(), true, classGuid); + } } else if (chat_id != 0) { - currentChat = MessagesController.getInstance(currentAccount).getChat(chat_id); + currentChat = getMessagesController().getChat(chat_id); if (currentChat == null) { final CountDownLatch countDownLatch = new CountDownLatch(1); - MessagesStorage.getInstance(currentAccount).getStorageQueue().postRunnable(() -> { - currentChat = MessagesStorage.getInstance(currentAccount).getChat(chat_id); + getMessagesStorage().getStorageQueue().postRunnable(() -> { + currentChat = getMessagesStorage().getChat(chat_id); countDownLatch.countDown(); }); try { @@ -1046,7 +1300,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. FileLog.e(e); } if (currentChat != null) { - MessagesController.getInstance(currentAccount).putChat(currentChat, true); + getMessagesController().putChat(currentChat, true); } else { return false; } @@ -1057,8 +1311,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else { participantsMap = null; } - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.chatInfoDidLoad); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.chatOnlineCountDidLoad); + getNotificationCenter().addObserver(this, NotificationCenter.chatInfoDidLoad); + getNotificationCenter().addObserver(this, NotificationCenter.chatOnlineCountDidLoad); sortedUsers = new ArrayList<>(); updateOnlineCount(); @@ -1066,7 +1320,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. chatInfo = getMessagesController().getChatFull(chat_id); } if (ChatObject.isChannel(currentChat)) { - MessagesController.getInstance(currentAccount).loadFullChat(chat_id, classGuid, true); + getMessagesController().loadFullChat(chat_id, classGuid, true); } else if (chatInfo == null) { chatInfo = getMessagesStorage().loadChatInfo(chat_id, null, false, false); } @@ -1078,15 +1332,37 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } sharedMediaPreloader.addDelegate(this); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.updateInterfaces); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.didReceiveNewMessages); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.closeChats); + getNotificationCenter().addObserver(this, NotificationCenter.updateInterfaces); + getNotificationCenter().addObserver(this, NotificationCenter.didReceiveNewMessages); + getNotificationCenter().addObserver(this, NotificationCenter.closeChats); NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.emojiDidLoad); updateRowsIds(); + if (arguments.containsKey("nearby_distance")) { + preloadGreetingsSticker(); + } + return true; } + private void preloadGreetingsSticker() { + TLRPC.TL_messages_getStickers req = new TLRPC.TL_messages_getStickers(); + req.emoticon = "\uD83D\uDC4B" + Emoji.fixEmoji("⭐"); + ConnectionsManager.getInstance(UserConfig.selectedAccount).sendRequest(req, (response, error) -> { + if (response instanceof TLRPC.TL_messages_stickers) { + ArrayList list = ((TLRPC.TL_messages_stickers) response).stickers; + if (!list.isEmpty()) { + TLRPC.Document sticker = list.get(Math.abs(new Random().nextInt() % list.size())); + AndroidUtilities.runOnUIThread(() -> { + ImageReceiver receiver = new ImageReceiver(); + receiver.setImage(ImageLocation.getForDocument(sticker), ChatGreetingsView.createFilter(sticker), null, null, sticker, 0); + }); + preloadedSticker = sticker; + } + } + }); + } + @Override public void onFragmentDestroy() { super.onFragmentDestroy(); @@ -1100,24 +1376,30 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. sharedMediaPreloader.removeDelegate(this); } - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.updateInterfaces); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.closeChats); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.didReceiveNewMessages); + getNotificationCenter().removeObserver(this, NotificationCenter.updateInterfaces); + getNotificationCenter().removeObserver(this, NotificationCenter.closeChats); + getNotificationCenter().removeObserver(this, NotificationCenter.didReceiveNewMessages); NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.emojiDidLoad); if (avatarsViewPager != null) { avatarsViewPager.onDestroy(); } if (user_id != 0) { - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.contactsDidLoad); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.encryptedChatCreated); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.encryptedChatUpdated); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.blockedUsersDidLoad); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.botInfoDidLoad); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.userInfoDidLoad); - MessagesController.getInstance(currentAccount).cancelLoadFullUser(user_id); + getNotificationCenter().removeObserver(this, NotificationCenter.contactsDidLoad); + getNotificationCenter().removeObserver(this, NotificationCenter.encryptedChatCreated); + getNotificationCenter().removeObserver(this, NotificationCenter.encryptedChatUpdated); + getNotificationCenter().removeObserver(this, NotificationCenter.blockedUsersDidLoad); + getNotificationCenter().removeObserver(this, NotificationCenter.botInfoDidLoad); + getNotificationCenter().removeObserver(this, NotificationCenter.userInfoDidLoad); + getMessagesController().cancelLoadFullUser(user_id); } else if (chat_id != 0) { - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.chatInfoDidLoad); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.chatOnlineCountDidLoad); + getNotificationCenter().removeObserver(this, NotificationCenter.chatInfoDidLoad); + getNotificationCenter().removeObserver(this, NotificationCenter.chatOnlineCountDidLoad); + } + if (avatarImage != null) { + avatarImage.setImageDrawable(null); + } + if (imageUpdater != null) { + imageUpdater.clear(); } } @@ -1127,7 +1409,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override public boolean onTouchEvent(MotionEvent event) { - avatarImage.getHitRect(rect); + avatarContainer.getHitRect(rect); if (rect.contains((int) event.getX(), (int) event.getY())) { return false; } @@ -1141,7 +1423,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. actionBar.setCastShadows(false); actionBar.setAddToContainer(false); actionBar.setClipContent(true); - actionBar.setOccupyStatusBar(Build.VERSION.SDK_INT >= 21 && !AndroidUtilities.isTablet()); + actionBar.setOccupyStatusBar(Build.VERSION.SDK_INT >= 21 && !AndroidUtilities.isTablet() && !inBubbleMode); return actionBar; } @@ -1149,6 +1431,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public View createView(Context context) { Theme.createProfileResources(context); + searchTransitionOffset = 0; + searchTransitionProgress = 1f; + searchMode = false; hasOwnBackground = true; extraHeight = AndroidUtilities.dp(88f); actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { @@ -1160,20 +1445,20 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (id == -1) { finishFragment(); } else if (id == block_contact) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + TLRPC.User user = getMessagesController().getUser(user_id); if (user == null) { return; } if (!isBot || MessagesController.isSupportUser(user)) { if (userBlocked) { - MessagesController.getInstance(currentAccount).unblockUser(user_id); + getMessagesController().unblockUser(user_id); AlertsCreator.showSimpleToast(ProfileActivity.this, LocaleController.getString("UserUnblocked", R.string.UserUnblocked)); } else { if (reportSpam) { AlertsCreator.showBlockReportSpamAlert(ProfileActivity.this, user_id, user, null, currentEncryptedChat, false, null, param -> { if (param == 1) { - NotificationCenter.getInstance(currentAccount).removeObserver(ProfileActivity.this, NotificationCenter.closeChats); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); + getNotificationCenter().removeObserver(ProfileActivity.this, NotificationCenter.closeChats); + getNotificationCenter().postNotificationName(NotificationCenter.closeChats); playProfileAnimation = 0; finishFragment(); } else { @@ -1185,7 +1470,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. builder.setTitle(LocaleController.getString("BlockUser", R.string.BlockUser)); builder.setMessage(AndroidUtilities.replaceTags(LocaleController.formatString("AreYouSureBlockContact2", R.string.AreYouSureBlockContact2, ContactsController.formatName(user.first_name, user.last_name)))); builder.setPositiveButton(LocaleController.getString("BlockContact", R.string.BlockContact), (dialogInterface, i) -> { - MessagesController.getInstance(currentAccount).blockUser(user_id); + getMessagesController().blockUser(user_id); AlertsCreator.showSimpleToast(ProfileActivity.this, LocaleController.getString("UserBlocked", R.string.UserBlocked)); }); builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); @@ -1199,15 +1484,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } else { if (!userBlocked) { - MessagesController.getInstance(currentAccount).blockUser(user_id); + getMessagesController().blockUser(user_id); } else { - MessagesController.getInstance(currentAccount).unblockUser(user_id); - SendMessagesHelper.getInstance(currentAccount).sendMessage("/start", user_id, null, null, false, null, null, null, true, 0); + getMessagesController().unblockUser(user_id); + getSendMessagesHelper().sendMessage("/start", user_id, null, null, false, null, null, null, true, 0); finishFragment(); } } } else if (id == add_contact) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + TLRPC.User user = getMessagesController().getUser(user_id); Bundle args = new Bundle(); args.putInt("user_id", user.id); args.putBoolean("addContact", true); @@ -1226,7 +1511,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. args.putInt("user_id", user_id); presentFragment(new ContactAddActivity(args)); } else if (id == delete_contact) { - final TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + final TLRPC.User user = getMessagesController().getUser(user_id); if (user == null || getParentActivity() == null) { return; } @@ -1236,7 +1521,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. builder.setPositiveButton(LocaleController.getString("Delete", R.string.Delete), (dialogInterface, i) -> { ArrayList arrayList = new ArrayList<>(); arrayList.add(user); - ContactsController.getInstance(currentAccount).deleteContact(arrayList); + getContactsController().deleteContact(arrayList); }); builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); AlertDialog dialog = builder.create(); @@ -1256,7 +1541,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. fragment.setInfo(chatInfo); presentFragment(fragment); } else if (id == invite_to_group) { - final TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + final TLRPC.User user = getMessagesController().getUser(user_id); if (user == null) { return; } @@ -1270,13 +1555,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. Bundle args1 = new Bundle(); args1.putBoolean("scrollToTopOnResume", true); args1.putInt("chat_id", -(int) did); - if (!MessagesController.getInstance(currentAccount).checkCanOpenChat(args1, fragment1)) { + if (!getMessagesController().checkCanOpenChat(args1, fragment1)) { return; } - NotificationCenter.getInstance(currentAccount).removeObserver(ProfileActivity.this, NotificationCenter.closeChats); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); - MessagesController.getInstance(currentAccount).addUserToChat(-(int) did, user, null, 0, null, ProfileActivity.this, null); + getNotificationCenter().removeObserver(ProfileActivity.this, NotificationCenter.closeChats); + getNotificationCenter().postNotificationName(NotificationCenter.closeChats); + getMessagesController().addUserToChat(-(int) did, user, null, 0, null, ProfileActivity.this, null); presentFragment(new ChatActivity(args1), true); removeSelfFromStack(); }); @@ -1285,24 +1570,24 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. try { String text = null; if (user_id != 0) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + TLRPC.User user = getMessagesController().getUser(user_id); if (user == null) { return; } if (botInfo != null && userInfo != null && !TextUtils.isEmpty(userInfo.about) && id == share) { - text = String.format("%s https://" + MessagesController.getInstance(currentAccount).linkPrefix + "/%s", userInfo.about, user.username); + text = String.format("%s https://" + getMessagesController().linkPrefix + "/%s", userInfo.about, user.username); } else { - text = String.format("https://" + MessagesController.getInstance(currentAccount).linkPrefix + "/%s", user.username); + text = String.format("https://" + getMessagesController().linkPrefix + "/%s", user.username); } } else if (chat_id != 0) { - TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(chat_id); + TLRPC.Chat chat = getMessagesController().getChat(chat_id); if (chat == null) { return; } if (chatInfo != null && !TextUtils.isEmpty(chatInfo.about) && id == share) { - text = String.format("%s\nhttps://" + MessagesController.getInstance(currentAccount).linkPrefix + "/%s", chatInfo.about, chat.username); + text = String.format("%s\nhttps://" + getMessagesController().linkPrefix + "/%s", chatInfo.about, chat.username); } else { - text = String.format("https://" + MessagesController.getInstance(currentAccount).linkPrefix + "/%s", chat.username); + text = String.format("https://" + getMessagesController().linkPrefix + "/%s", chat.username); } } if (TextUtils.isEmpty(text)) { @@ -1331,12 +1616,12 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else { return; } - MediaDataController.getInstance(currentAccount).installShortcut(did); + getMediaDataController().installShortcut(did); } catch (Exception e) { FileLog.e(e); } } else if (id == call_item) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + TLRPC.User user = getMessagesController().getUser(user_id); if (user != null) { VoIPHelper.startCall(user, getParentActivity(), userInfo); } @@ -1351,8 +1636,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else if (id == add_member) { openAddMember(); } else if (id == statistics) { + TLRPC.Chat chat = getMessagesController().getChat(chat_id); Bundle args = new Bundle(); - args.putInt("chat_id",chat_id); + args.putInt("chat_id", chat_id); + args.putBoolean("is_megagroup", chat.megagroup); StatisticActivity fragment = new StatisticActivity(args); presentFragment(fragment); } else if (id == start_secret_chat) { @@ -1361,7 +1648,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. builder.setMessage(LocaleController.getString("AreYouSureSecretChat", R.string.AreYouSureSecretChat)); builder.setPositiveButton(LocaleController.getString("Start", R.string.Start), (dialogInterface, i) -> { creatingChat = true; - SecretChatHelper.getInstance(currentAccount).startSecretChat(getParentActivity(), MessagesController.getInstance(currentAccount).getUser(user_id)); + getSecretChatHelper().startSecretChat(getParentActivity(), getMessagesController().getUser(user_id)); }); builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); showDialog(builder.create()); @@ -1374,12 +1661,131 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return; } ImageLocation location = avatarsViewPager.getImageLocation(avatarsViewPager.getRealPosition()); - if (location != null) { - File f = FileLoader.getPathToAttach(location.location, true); - if (f != null && f.exists()) { - MediaController.saveFile(f.toString(), getParentActivity(), 0, null, null); - } + if (location == null) { + return; } + File f = FileLoader.getPathToAttach(location.location, location.imageType == FileLoader.IMAGE_TYPE_ANIMATION ? "mp4" : null, true); + if (f != null && f.exists()) { + MediaController.saveFile(f.toString(), getParentActivity(), 0, null, null); + } + } else if (id == edit_name) { + presentFragment(new ChangeNameActivity()); + } else if (id == logout) { + presentFragment(new LogoutActivity()); + } else if (id == set_as_main) { + int position = avatarsViewPager.getRealPosition(); + TLRPC.Photo photo = avatarsViewPager.getPhoto(position); + if (photo == null) { + return; + } + avatarsViewPager.startMovePhotoToBegin(position); + + TLRPC.TL_photos_updateProfilePhoto req = new TLRPC.TL_photos_updateProfilePhoto(); + req.id = new TLRPC.TL_inputPhoto(); + req.id.id = photo.id; + req.id.access_hash = photo.access_hash; + req.id.file_reference = photo.file_reference; + UserConfig userConfig = getUserConfig(); + getConnectionsManager().sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> { + avatarsViewPager.finishSettingMainPhoto(); + if (response instanceof TLRPC.TL_photos_photo) { + TLRPC.TL_photos_photo photos_photo = (TLRPC.TL_photos_photo) response; + getMessagesController().putUsers(photos_photo.users, false); + TLRPC.User user = getMessagesController().getUser(userConfig.clientUserId); + if (photos_photo.photo instanceof TLRPC.TL_photo) { + avatarsViewPager.replaceFirstPhoto(photo, photos_photo.photo); + if (user != null) { + user.photo.photo_id = photos_photo.photo.id; + userConfig.setCurrentUser(user); + userConfig.saveConfig(true); + } + } + } + })); + undoView.showWithAction(user_id, UndoView.ACTION_PROFILE_PHOTO_CHANGED, photo.video_sizes.isEmpty() ? null : 1); + TLRPC.User user = getMessagesController().getUser(userConfig.clientUserId); + + TLRPC.PhotoSize bigSize = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 800); + if (user != null) { + TLRPC.PhotoSize smallSize = FileLoader.getClosestPhotoSizeWithSize(photo.sizes, 90); + user.photo.photo_id = photo.id; + user.photo.photo_small = smallSize.location; + user.photo.photo_big = bigSize.location; + userConfig.setCurrentUser(user); + userConfig.saveConfig(true); + NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.mainUserInfoChanged); + updateProfileData(); + } + avatarsViewPager.commitMoveToBegin(); + } else if (id == edit_avatar) { + int position = avatarsViewPager.getRealPosition(); + ImageLocation location = avatarsViewPager.getImageLocation(position); + if (location == null) { + return; + } + + File f = FileLoader.getPathToAttach(PhotoViewer.getFileLocation(location), PhotoViewer.getFileLocationExt(location), true); + boolean isVideo = location.imageType == FileLoader.IMAGE_TYPE_ANIMATION; + String thumb; + if (isVideo) { + ImageLocation imageLocation = avatarsViewPager.getRealImageLocation(position); + thumb = FileLoader.getPathToAttach(PhotoViewer.getFileLocation(imageLocation), PhotoViewer.getFileLocationExt(imageLocation), true).getAbsolutePath(); + } else { + thumb = null; + } + imageUpdater.openPhotoForEdit(f.getAbsolutePath(), thumb, 0, isVideo); + } else if (id == delete_avatar) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + ImageLocation location = avatarsViewPager.getImageLocation(avatarsViewPager.getRealPosition()); + if (location == null) { + return; + } + if (location.imageType == FileLoader.IMAGE_TYPE_ANIMATION) { + builder.setTitle(LocaleController.getString("AreYouSureDeleteVideoTitle", R.string.AreYouSureDeleteVideoTitle)); + builder.setMessage(LocaleController.formatString("AreYouSureDeleteVideo", R.string.AreYouSureDeleteVideo)); + } else { + builder.setTitle(LocaleController.getString("AreYouSureDeletePhotoTitle", R.string.AreYouSureDeletePhotoTitle)); + builder.setMessage(LocaleController.formatString("AreYouSureDeletePhoto", R.string.AreYouSureDeletePhoto)); + } + builder.setPositiveButton(LocaleController.getString("Delete", R.string.Delete), (dialogInterface, i) -> { + int position = avatarsViewPager.getRealPosition(); + TLRPC.Photo photo = avatarsViewPager.getPhoto(position); + if (avatarsViewPager.getRealCount() == 1) { + setForegroundImage(true); + } + if (photo == null || avatarsViewPager.getRealPosition() == 0) { + getMessagesController().deleteUserPhoto(null); + } else { + TLRPC.TL_inputPhoto inputPhoto = new TLRPC.TL_inputPhoto(); + inputPhoto.id = photo.id; + inputPhoto.access_hash = photo.access_hash; + inputPhoto.file_reference = photo.file_reference; + if (inputPhoto.file_reference == null) { + inputPhoto.file_reference = new byte[0]; + } + getMessagesController().deleteUserPhoto(inputPhoto); + getMessagesStorage().clearUserPhoto(user_id, photo.id); + } + if (avatarsViewPager.removePhotoAtIndex(position)) { + avatarsViewPager.setVisibility(View.GONE); + avatarImage.setForegroundAlpha(1f); + avatarContainer.setVisibility(View.VISIBLE); + doNotSetForeground = true; + final View view = layoutManager.findViewByPosition(0); + if (view != null) { + listView.smoothScrollBy(0, view.getTop() - AndroidUtilities.dp(88), CubicBezierInterpolator.EASE_OUT_QUINT); + } + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + AlertDialog alertDialog = builder.create(); + showDialog(alertDialog); + TextView button = (TextView) alertDialog.getButton(DialogInterface.BUTTON_POSITIVE); + if (button != null) { + button.setTextColor(Theme.getColor(Theme.key_dialogTextRed2)); + } + } else if (id == add_photo) { + writeButton.callOnClick(); } } }); @@ -1417,7 +1823,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } listView.stopScroll(); - avatarImage.setVisibility(expanded ? INVISIBLE : VISIBLE); + avatarContainer.setVisibility(expanded ? INVISIBLE : VISIBLE); nameTextView[1].setVisibility(expanded ? INVISIBLE : VISIBLE); onlineTextView[1].setVisibility(expanded ? INVISIBLE : VISIBLE); onlineTextView[2].setVisibility(expanded ? INVISIBLE : VISIBLE); @@ -1434,13 +1840,56 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. sharedMediaLayout.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT)); ActionBarMenu menu = actionBar.createMenu(); + + if (imageUpdater != null) { + searchItem = menu.addItem(search_button, R.drawable.ic_ab_search).setIsSearchField(true).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { + + @Override + public Animator getCustomToggleTransition() { + searchMode = !searchMode; + if (!searchMode) { + searchItem.clearFocusOnSearchView(); + } + return searchExpandTransition(searchMode); + } + + @Override + public void onTextChanged(EditText editText) { + searchAdapter.search(editText.getText().toString().toLowerCase()); + } + }); + searchItem.setContentDescription(LocaleController.getString("SearchInSettings", R.string.SearchInSettings)); + searchItem.setSearchFieldHint(LocaleController.getString("SearchInSettings", R.string.SearchInSettings)); + sharedMediaLayout.getSearchItem().setVisibility(View.GONE); + } + callItem = menu.addItem(call_item, R.drawable.ic_call); + callItem.setContentDescription(LocaleController.getString("Call", R.string.Call)); editItem = menu.addItem(edit_channel, R.drawable.group_edit_profile); + editItem.setContentDescription(LocaleController.getString("Edit", R.string.Edit)); otherItem = menu.addItem(10, R.drawable.ic_ab_other); + otherItem.setContentDescription(LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions)); + + int scrollTo; + int scrollToPosition = 0; + Object writeButtonTag = null; + if (listView != null && imageUpdater != null) { + scrollTo = layoutManager.findFirstVisibleItemPosition(); + View topView = layoutManager.findViewByPosition(scrollTo); + if (topView != null) { + scrollToPosition = topView.getTop(); + } else { + scrollTo = -1; + } + writeButtonTag = writeButton.getTag(); + } else { + scrollTo = -1; + } createActionBarMenu(); listAdapter = new ListAdapter(context); + searchAdapter = new SearchAdapter(context); avatarDrawable = new AvatarDrawable(); avatarDrawable.setProfile(true); @@ -1448,6 +1897,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private boolean ignoreLayout; private boolean firstLayout = true; + private Paint whitePaint = new Paint(); + private Paint grayPaint = new Paint(); @Override public boolean hasOverlappingRendering() { @@ -1463,6 +1914,12 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. layoutParams.topMargin = actionBarHeight; } } + if (searchListView != null) { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) searchListView.getLayoutParams(); + if (layoutParams.topMargin != actionBarHeight) { + layoutParams.topMargin = actionBarHeight; + } + } super.onMeasure(widthMeasureSpec, heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); @@ -1493,6 +1950,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. ignoreLayout = true; if (expandPhoto) { + if (searchItem != null) { + searchItem.setAlpha(0.0f); + searchItem.setEnabled(false); + } nameTextView[1].setTextColor(Color.WHITE); onlineTextView[1].setTextColor(Color.argb(179, 255, 255, 255)); idTextView.setTextColor(Color.argb(179, 255, 255, 255)); @@ -1501,7 +1962,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. overlaysView.setOverlaysVisible(); overlaysView.setAlphaValue(1.0f, false); avatarImage.setForegroundAlpha(1.0f); - avatarImage.setVisibility(View.GONE); + avatarContainer.setVisibility(View.GONE); avatarsViewPager.resetCurrentItem(); avatarsViewPager.setVisibility(View.VISIBLE); expandPhoto = false; @@ -1511,6 +1972,12 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. isPulledDown = true; if (otherItem != null) { otherItem.showSubItem(gallery_menu_save); + if (imageUpdater != null) { + otherItem.showSubItem(edit_avatar); + otherItem.showSubItem(delete_avatar); + otherItem.hideSubItem(logout); + otherItem.hideSubItem(edit_name); + } } currentExpanAnimatorFracture = 1.0f; @@ -1569,7 +2036,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (actionBar.isSearchFieldVisible()) { layoutManager.scrollToPositionWithOffset(sharedMediaRow, -paddingTop); layout = true; - } else if (!changed && pos != RecyclerView.NO_POSITION) { + } else if ((!changed || !allowPullingDown) && pos != RecyclerView.NO_POSITION) { layoutManager.scrollToPositionWithOffset(pos, top - paddingTop); layout = true; } @@ -1579,7 +2046,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } if (layout) { measureChildWithMargins(listView, widthMeasureSpec, 0, heightMeasureSpec, 0); - listView.layout(0, actionBarHeight, listView.getMeasuredWidth(), actionBarHeight + listView.getMeasuredHeight()); + try { + listView.layout(0, actionBarHeight, listView.getMeasuredWidth(), actionBarHeight + listView.getMeasuredHeight()); + } catch (Exception e) { + FileLog.e(e); + } } ignoreLayout = false; } @@ -1599,13 +2070,45 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } super.requestLayout(); } + + @Override + public void onDraw(Canvas c) { + whitePaint.setColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + if (listView.getVisibility() == VISIBLE) { + grayPaint.setColor(Theme.getColor(Theme.key_windowBackgroundGray)); + if (transitionAnimationInProress) { + whitePaint.setAlpha((int) (255 * listView.getAlpha())); + } + if (transitionAnimationInProress) { + grayPaint.setAlpha((int) (255 * listView.getAlpha())); + } + + int top = listView.getTop(); + int count = listView.getChildCount(); + Paint paint; + for (int a = 0; a <= count; a++) { + if (a < count) { + View child = listView.getChildAt(a); + int bottom = listView.getTop() + child.getBottom(); + c.drawRect(listView.getX(), top, listView.getX() + listView.getMeasuredWidth(), bottom, child.getBackground() != null ? grayPaint : whitePaint); + top = bottom; + } else { + if (top < listView.getBottom()) { + c.drawRect(listView.getX(), top, listView.getX() + listView.getMeasuredWidth(), listView.getBottom(), grayPaint); + } + } + } + } else { + int top = searchListView.getTop(); + c.drawRect(0, top + extraHeight + searchTransitionOffset, getMeasuredWidth(), top + getMeasuredHeight(), whitePaint); + } + } }; + fragmentView.setWillNotDraw(false); FrameLayout frameLayout = (FrameLayout) fragmentView; listView = new RecyclerListView(context) { - private final Paint paint = new Paint(); - private VelocityTracker velocityTracker; @Override @@ -1624,37 +2127,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } @Override - public void onDraw(Canvas c) { - ViewHolder holder; - if (sharedMediaRow != -1) { - holder = null; - } else if (lastSectionRow != -1) { - holder = findViewHolderForAdapterPosition(lastSectionRow); - } else if (membersSectionRow != -1 && (sharedMediaRow == -1 || membersSectionRow > sharedMediaRow)) { - holder = findViewHolderForAdapterPosition(membersSectionRow); - } else if (settingsSectionRow != -1) { - holder = findViewHolderForAdapterPosition(settingsSectionRow); - } else if (infoSectionRow != -1) { - holder = findViewHolderForAdapterPosition(infoSectionRow); - } else { - holder = null; - } - int bottom; - int height = getMeasuredHeight(); - if (holder != null) { - bottom = holder.itemView.getBottom(); - if (holder.itemView.getBottom() >= height) { - bottom = height; - } - } else { - bottom = height; - } - - paint.setColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - c.drawRect(0, 0, getMeasuredWidth(), bottom, paint); - if (bottom != height) { - paint.setColor(Theme.getColor(Theme.key_windowBackgroundGray)); - c.drawRect(0, bottom, getMeasuredWidth(), height, paint); + public void invalidate() { + super.invalidate(); + if (fragmentView != null) { + fragmentView.invalidate(); } } @@ -1698,14 +2174,22 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } }; listView.setVerticalScrollBarEnabled(false); - listView.setItemAnimator(null); - listView.setLayoutAnimation(null); + if (imageUpdater == null) { + listView.setItemAnimator(null); + listView.setLayoutAnimation(null); + } else { + DefaultItemAnimator itemAnimator = (DefaultItemAnimator) listView.getItemAnimator(); + itemAnimator.setSupportsChangeAnimations(false); + itemAnimator.setDelayAnimations(false); + } listView.setClipToPadding(false); + listView.setHideIfEmpty(false); + layoutManager = new LinearLayoutManager(context) { @Override public boolean supportsPredictiveItemAnimations() { - return false; + return imageUpdater != null; } @Override @@ -1716,7 +2200,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (!allowPullingDown && canScroll > dy) { dy = canScroll; if (avatarsViewPager.hasImages() && avatarImage.getImageReceiver().hasNotThumb() && !isInLandscapeMode && !AndroidUtilities.isTablet()) { - allowPullingDown = true; + allowPullingDown = avatarBig == null; } } else if (allowPullingDown) { if (dy >= canScroll) { @@ -1737,158 +2221,442 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. listView.setGlowColor(0); listView.setAdapter(listAdapter); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); - listView.setOnItemClickListener((view, position, x, y) -> { - if (getParentActivity() == null) { - return; - } - if (position == settingsKeyRow) { - Bundle args = new Bundle(); - args.putInt("chat_id", (int) (dialog_id >> 32)); - presentFragment(new IdenticonActivity(args)); - } else if (position == settingsTimerRow) { - showDialog(AlertsCreator.createTTLAlert(getParentActivity(), currentEncryptedChat).create()); - } else if (position == notificationsRow) { - if (LocaleController.isRTL && x <= AndroidUtilities.dp(76) || !LocaleController.isRTL && x >= view.getMeasuredWidth() - AndroidUtilities.dp(76)) { - NotificationsCheckCell checkCell = (NotificationsCheckCell) view; - boolean checked = !checkCell.isChecked(); + listView.setOnItemClickListener(new RecyclerListView.OnItemClickListenerExtended() { - boolean defaultEnabled = NotificationsController.getInstance(currentAccount).isGlobalNotificationsEnabled(did); + private int pressCount = 0; - if (checked) { - SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); - SharedPreferences.Editor editor = preferences.edit(); - if (defaultEnabled) { - editor.remove("notify2_" + did); - } else { - editor.putInt("notify2_" + did, 0); - } - MessagesStorage.getInstance(currentAccount).setDialogFlags(did, 0); - editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance(currentAccount).dialogs_dict.get(did); - if (dialog != null) { - dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); - } - NotificationsController.getInstance(currentAccount).updateServerNotificationsSettings(did); - } else { - int untilTime = Integer.MAX_VALUE; - SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); - SharedPreferences.Editor editor = preferences.edit(); - long flags; - if (!defaultEnabled) { - editor.remove("notify2_" + did); - flags = 0; - } else { - editor.putInt("notify2_" + did, 2); - flags = 1; - } - NotificationsController.getInstance(currentAccount).removeNotificationsForDialog(did); - MessagesStorage.getInstance(currentAccount).setDialogFlags(did, flags); - editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance(currentAccount).dialogs_dict.get(did); - if (dialog != null) { - dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); - if (defaultEnabled) { - dialog.notify_settings.mute_until = untilTime; - } - } - NotificationsController.getInstance(currentAccount).updateServerNotificationsSettings(did); - } - checkCell.setChecked(checked); - RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForPosition(notificationsRow); - if (holder != null) { - listAdapter.onBindViewHolder(holder, notificationsRow); - } + @Override public void onItemClick(View view, int position, float x, float y) { + if (ProfileActivity.this.getParentActivity() == null) { return; } - AlertsCreator.showCustomNotificationsDialog(ProfileActivity.this, did, -1, null, currentAccount, param -> listAdapter.notifyItemChanged(notificationsRow)); - } else if (position == unblockRow) { - MessagesController.getInstance(currentAccount).unblockUser(user_id); - AlertsCreator.showSimpleToast(ProfileActivity.this, LocaleController.getString("UserUnblocked", R.string.UserUnblocked)); - } else if (position == sendMessageRow) { - writeButton.callOnClick(); - } else if (position == reportRow) { - AlertsCreator.createReportAlert(getParentActivity(), getDialogId(), 0, ProfileActivity.this); - } else if (position >= membersStartRow && position < membersEndRow) { - TLRPC.ChatParticipant participant; - if (!sortedUsers.isEmpty()) { - participant = chatInfo.participants.participants.get(sortedUsers.get(position - membersStartRow)); - } else { - participant = chatInfo.participants.participants.get(position - membersStartRow); - } - onMemberClick(participant, false); - } else if (position == addMemberRow) { - openAddMember(); - } else if (position == usernameRow) { - if (currentChat != null) { - try { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - if (!TextUtils.isEmpty(chatInfo.about)) { - intent.putExtra(Intent.EXTRA_TEXT, currentChat.title + "\n" + chatInfo.about + "\nhttps://" + MessagesController.getInstance(currentAccount).linkPrefix + "/" + currentChat.username); + if (position == settingsKeyRow) { + Bundle args = new Bundle(); + args.putInt("chat_id", (int) (dialog_id >> 32)); + ProfileActivity.this.presentFragment(new IdenticonActivity(args)); + } else if (position == settingsTimerRow) { + ProfileActivity.this.showDialog(AlertsCreator.createTTLAlert(ProfileActivity.this.getParentActivity(), currentEncryptedChat).create()); + } else if (position == notificationsRow) { + if (LocaleController.isRTL && x <= AndroidUtilities.dp(76) || !LocaleController.isRTL && x >= view.getMeasuredWidth() - AndroidUtilities.dp(76)) { + NotificationsCheckCell checkCell = (NotificationsCheckCell) view; + boolean checked = !checkCell.isChecked(); + + boolean defaultEnabled = ProfileActivity.this.getNotificationsController().isGlobalNotificationsEnabled(did); + + if (checked) { + SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); + SharedPreferences.Editor editor = preferences.edit(); + if (defaultEnabled) { + editor.remove("notify2_" + did); + } else { + editor.putInt("notify2_" + did, 0); + } + ProfileActivity.this.getMessagesStorage().setDialogFlags(did, 0); + editor.commit(); + TLRPC.Dialog dialog = ProfileActivity.this.getMessagesController().dialogs_dict.get(did); + if (dialog != null) { + dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); + } } else { - intent.putExtra(Intent.EXTRA_TEXT, currentChat.title + "\nhttps://" + MessagesController.getInstance(currentAccount).linkPrefix + "/" + currentChat.username); + int untilTime = Integer.MAX_VALUE; + SharedPreferences preferences = MessagesController.getNotificationsSettings(currentAccount); + SharedPreferences.Editor editor = preferences.edit(); + long flags; + if (!defaultEnabled) { + editor.remove("notify2_" + did); + flags = 0; + } else { + editor.putInt("notify2_" + did, 2); + flags = 1; + } + ProfileActivity.this.getNotificationsController().removeNotificationsForDialog(did); + ProfileActivity.this.getMessagesStorage().setDialogFlags(did, flags); + editor.commit(); + TLRPC.Dialog dialog = ProfileActivity.this.getMessagesController().dialogs_dict.get(did); + if (dialog != null) { + dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); + if (defaultEnabled) { + dialog.notify_settings.mute_until = untilTime; + } + } + } + ProfileActivity.this.getNotificationsController().updateServerNotificationsSettings(did); + checkCell.setChecked(checked); + RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findViewHolderForPosition(notificationsRow); + if (holder != null) { + listAdapter.onBindViewHolder(holder, notificationsRow); + } + return; + } + AlertsCreator.showCustomNotificationsDialog(ProfileActivity.this, did, -1, null, currentAccount, param -> listAdapter.notifyItemChanged(notificationsRow)); + } else if (position == unblockRow) { + ProfileActivity.this.getMessagesController().unblockUser(user_id); + AlertsCreator.showSimpleToast(ProfileActivity.this, LocaleController.getString("UserUnblocked", R.string.UserUnblocked)); + } else if (position == sendMessageRow) { + writeButton.callOnClick(); + } else if (position == reportRow) { + AlertsCreator.createReportAlert(ProfileActivity.this.getParentActivity(), ProfileActivity.this.getDialogId(), 0, ProfileActivity.this); + } else if (position >= membersStartRow && position < membersEndRow) { + TLRPC.ChatParticipant participant; + if (!sortedUsers.isEmpty()) { + participant = chatInfo.participants.participants.get(sortedUsers.get(position - membersStartRow)); + } else { + participant = chatInfo.participants.participants.get(position - membersStartRow); + } + ProfileActivity.this.onMemberClick(participant, false); + } else if (position == addMemberRow) { + ProfileActivity.this.openAddMember(); + } else if (position == usernameRow) { + if (currentChat != null) { + try { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + if (!TextUtils.isEmpty(chatInfo.about)) { + intent.putExtra(Intent.EXTRA_TEXT, currentChat.title + "\n" + chatInfo.about + "\nhttps://" + ProfileActivity.this.getMessagesController().linkPrefix + "/" + currentChat.username); + } else { + intent.putExtra(Intent.EXTRA_TEXT, currentChat.title + "\nhttps://" + ProfileActivity.this.getMessagesController().linkPrefix + "/" + currentChat.username); + } + ProfileActivity.this.getParentActivity().startActivityForResult(Intent.createChooser(intent, LocaleController.getString("BotShare", R.string.BotShare)), 500); + } catch (Exception e) { + FileLog.e(e); + } + } + } else if (position == locationRow) { + if (chatInfo.location instanceof TLRPC.TL_channelLocation) { + LocationActivity fragment = new LocationActivity(LocationActivity.LOCATION_TYPE_GROUP_VIEW); + fragment.setChatLocation(chat_id, (TLRPC.TL_channelLocation) chatInfo.location); + ProfileActivity.this.presentFragment(fragment); + } + } else if (position == joinRow) { + ProfileActivity.this.getMessagesController().addUserToChat(currentChat.id, ProfileActivity.this.getUserConfig().getCurrentUser(), null, 0, null, ProfileActivity.this, null); + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.closeSearchByActiveAction); + } else if (position == subscribersRow) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat_id); + args.putInt("type", ChatUsersActivity.TYPE_USERS); + ChatUsersActivity fragment = new ChatUsersActivity(args); + fragment.setInfo(chatInfo); + ProfileActivity.this.presentFragment(fragment); + } else if (position == administratorsRow) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat_id); + args.putInt("type", ChatUsersActivity.TYPE_ADMIN); + ChatUsersActivity fragment = new ChatUsersActivity(args); + fragment.setInfo(chatInfo); + ProfileActivity.this.presentFragment(fragment); + } else if (position == blockedUsersRow) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat_id); + args.putInt("type", ChatUsersActivity.TYPE_BANNED); + ChatUsersActivity fragment = new ChatUsersActivity(args); + fragment.setInfo(chatInfo); + ProfileActivity.this.presentFragment(fragment); + } else if (position == notificationRow) { + presentFragment(new NotificationsSettingsActivity()); + } else if (position == privacyRow) { + presentFragment(new PrivacySettingsActivity()); + } else if (position == dataRow) { + presentFragment(new DataSettingsActivity()); + } else if (position == chatRow) { + presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC)); + } else if (position == stickersRow) { + presentFragment(new StickersActivity(MediaDataController.TYPE_IMAGE)); + } else if (position == filtersRow) { + presentFragment(new FiltersSetupActivity()); + } else if (position == devicesRow) { + presentFragment(new SessionsActivity(0)); + } else if (position == nekoRow) { + presentFragment(new NekoSettingsActivity()); + } else if (position == questionRow) { + Browser.openUrl(getParentActivity(), "https://t.me/NekogramX"); + } else if (position == faqRow) { + Browser.openUrl(getParentActivity(), NekoXConfig.FAQ_URL); + } else if (position == policyRow) { + Browser.openUrl(getParentActivity(), LocaleController.getString("PrivacyPolicyUrl", R.string.PrivacyPolicyUrl)); + } else if (position == sendLogsRow) { + sendLogs(); + } else if (position == clearLogsRow) { + AlertDialog pro = AlertUtil.showProgress(getParentActivity()); + pro.show(); + UIUtil.runOnIoDispatcher(() -> { + FileUtil.delete(new File(EnvUtil.getTelegramPath(), "logs")); + ThreadUtil.sleep(100L); + LangsKt.uDismiss(pro); + }); + } else if (position == switchBackendRow) { + if (getParentActivity() == null) { + return; + } + AlertDialog.Builder builder1 = new AlertDialog.Builder(getParentActivity()); + builder1.setMessage(LocaleController.getString("AreYouSure", R.string.AreYouSure)); + builder1.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder1.setPositiveButton(LocaleController.getString("OK", R.string.OK), (dialogInterface, i) -> { + SharedConfig.pushAuthKey = null; + SharedConfig.pushAuthKeyId = null; + SharedConfig.saveConfig(); + ConnectionsManager.getInstance(currentAccount).switchBackend(); + }); + builder1.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder1.create()); + } else if (position == languageRow) { + presentFragment(new LanguageSelectActivity()); + } else if (position == usernameRow) { + presentFragment(new ChangeUsernameActivity()); + } else if (position == bioRow) { + if (userInfo != null) { + presentFragment(new ChangeBioActivity()); + } + } else if (position == numberRow) { + presentFragment(new ActionIntroActivity(ActionIntroActivity.ACTION_TYPE_CHANGE_PHONE_NUMBER)); + } else if (position == setAvatarRow) { + writeButton.callOnClick(); + } else if (position == versionRow) { + TextInfoPrivacyCell cell = (TextInfoPrivacyCell) view; + pressCount++; + if (pressCount == 8) { + NekoXConfig.developerModeEntrance = true; + } + BottomBuilder builder = new BottomBuilder(getParentActivity()); + String message = cell.getTextView().getText().toString(); + try { + if (!BuildVars.isMini) { + message += "\n" + Libv2ray.checkVersionX(); + } + } catch (Exception ignored) { + } + builder.addTitle(message); + String finalMessage = message; + builder.addItem(LocaleController.getString("Copy", R.string.Copy), R.drawable.baseline_content_copy_24, (it) -> { + AndroidUtilities.addToClipboard(finalMessage); + AlertUtil.showToast(LocaleController.getString("TextCopied", R.string.TextCopied)); + return Unit.INSTANCE; + }); + builder.addItem(BuildVars.LOGS_ENABLED ? LocaleController.getString("DebugMenuDisableLogs", R.string.DebugMenuDisableLogs) : LocaleController.getString("DebugMenuEnableLogs", R.string.DebugMenuEnableLogs), R.drawable.baseline_bug_report_24, (it) -> { + BuildVars.LOGS_ENABLED = !BuildVars.LOGS_ENABLED; + SharedPreferences sharedPreferences = ApplicationLoader.applicationContext.getSharedPreferences("systemConfig", Context.MODE_PRIVATE); + sharedPreferences.edit().putBoolean("logsEnabled", BuildVars.LOGS_ENABLED).apply(); + updateRowsIds(); + return Unit.INSTANCE; + }); + if (!BuildVars.isUnknown) { + builder.addItem(LocaleController.getString("CheckUpdate", R.string.CheckUpdate), R.drawable.baseline_system_update_24, (it) -> { + UpdateChecksKt.checkUpdate(ProfileActivity.this.getParentActivity()); + return Unit.INSTANCE; + }); + } + if (BuildConfig.BUILD_TYPE.startsWith("release")) { + builder.addItem(LocaleController.getString("SwitchVersion", R.string.SwitchVersion), R.drawable.baseline_replay_24, (it) -> { + Browser.openUrl(ProfileActivity.this.getParentActivity(), "https://github.com/NekoX-Dev/NekoX/releases"); + return Unit.INSTANCE; + }); + } + if (NekoXConfig.developerModeEntrance || NekoXConfig.developerMode) { + builder.addItem(LocaleController.getString("DeveloperSettings", R.string.DeveloperSettings), R.drawable.baseline_developer_mode_24, (it) -> { + BottomBuilder devBuilder = new BottomBuilder(ProfileActivity.this.getParentActivity()); + devBuilder.addTitle(LocaleController.getString("DevModeTitle", R.string.DevModeTitle), LocaleController.getString("DevModeNotice", R.string.DevModeNotice)); + devBuilder.addItem(LocaleController.getString("Continue", R.string.Continue), R.drawable.baseline_warning_24, true, (__) -> { + ProfileActivity.this.presentFragment(new NekoXSettingActivity()); + return Unit.INSTANCE; + }); + devBuilder.addCancelItem(); + devBuilder.show(); + return Unit.INSTANCE; + }); + } + builder.show(); + } else { + ProfileActivity.this.processOnClickOrPress(position); + } + } + }); + + listView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { + + private int pressCount = 0; + + @Override + public boolean onItemClick(View view, int position) { + if (position == versionRow) { + pressCount++; + if (pressCount >= 2 || BuildVars.DEBUG_PRIVATE_VERSION) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("DebugMenu", R.string.DebugMenu)); + CharSequence[] items; + items = new CharSequence[]{ + LocaleController.getString("DebugMenuImportContacts", R.string.DebugMenuImportContacts), + LocaleController.getString("DebugMenuReloadContacts", R.string.DebugMenuReloadContacts), + LocaleController.getString("DebugMenuResetContacts", R.string.DebugMenuResetContacts), + LocaleController.getString("DebugMenuResetDialogs", R.string.DebugMenuResetDialogs), + BuildVars.LOGS_ENABLED ? LocaleController.getString("DebugMenuDisableLogs", R.string.DebugMenuDisableLogs) : LocaleController.getString("DebugMenuEnableLogs", R.string.DebugMenuEnableLogs), + SharedConfig.inappCamera ? LocaleController.getString("DebugMenuDisableCamera", R.string.DebugMenuDisableCamera) : LocaleController.getString("DebugMenuEnableCamera", R.string.DebugMenuEnableCamera), + LocaleController.getString("DebugMenuClearMediaCache", R.string.DebugMenuClearMediaCache), + LocaleController.getString("DebugMenuCallSettings", R.string.DebugMenuCallSettings), + null, + BuildVars.DEBUG_PRIVATE_VERSION ? "Check for app updates" : null, + LocaleController.getString("DebugMenuReadAllDialogs", R.string.DebugMenuReadAllDialogs), + SharedConfig.pauseMusicOnRecord ? LocaleController.getString("DebugMenuDisablePauseMusic", R.string.DebugMenuDisablePauseMusic) : LocaleController.getString("DebugMenuEnablePauseMusic", R.string.DebugMenuEnablePauseMusic), + BuildVars.DEBUG_VERSION && !AndroidUtilities.isTablet() && Build.VERSION.SDK_INT >= 23 ? (SharedConfig.smoothKeyboard ? LocaleController.getString("DebugMenuDisableSmoothKeyboard", R.string.DebugMenuDisableSmoothKeyboard) : LocaleController.getString("DebugMenuEnableSmoothKeyboard", R.string.DebugMenuEnableSmoothKeyboard)) : null, + Build.VERSION.SDK_INT >= 29 ? (SharedConfig.chatBubbles ? "Disable chat bubbles" : "Enable chat bubbles") : null + }; + builder.setItems(items, (dialog, which) -> { + if (which == 0) { + getUserConfig().syncContacts = true; + getUserConfig().saveConfig(false); + getContactsController().forceImportContacts(); + } else if (which == 1) { + getContactsController().loadContacts(false, 0); + } else if (which == 2) { + getContactsController().resetImportedContacts(); + } else if (which == 3) { + getMessagesController().forceResetDialogs(); + } else if (which == 4) { + BuildVars.LOGS_ENABLED = !BuildVars.LOGS_ENABLED; + SharedPreferences sharedPreferences = ApplicationLoader.applicationContext.getSharedPreferences("systemConfig", Context.MODE_PRIVATE); + sharedPreferences.edit().putBoolean("logsEnabled", BuildVars.LOGS_ENABLED).commit(); + updateRowsIds(); + listAdapter.notifyDataSetChanged(); + } else if (which == 5) { + SharedConfig.toggleInappCamera(); + } else if (which == 6) { + getMessagesStorage().clearSentMedia(); + SharedConfig.setNoSoundHintShowed(false); + SharedPreferences.Editor editor = MessagesController.getGlobalMainSettings().edit(); + editor.remove("archivehint").remove("archivehint_l").remove("gifhint").remove("soundHint").remove("themehint").remove("filterhint").commit(); + SharedConfig.textSelectionHintShows = 0; + SharedConfig.lockRecordAudioVideoHint = 0; + SharedConfig.stickersReorderingHintUsed = false; + } else if (which == 7) { + VoIPHelper.showCallDebugSettings(getParentActivity()); + } else if (which == 8) { + SharedConfig.toggleRoundCamera16to9(); + } else if (which == 9) { + } else if (which == 10) { + getMessagesStorage().readAllDialogs(-1); + } else if (which == 11) { + SharedConfig.togglePauseMusicOnRecord(); + } else if (which == 12) { + SharedConfig.toggleSmoothKeyboard(); + if (SharedConfig.smoothKeyboard && getParentActivity() != null) { + getParentActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); + } + } else if (which == 13) { + SharedConfig.toggleChatBubbles(); + } + }); + 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(e); + } + } + return true; + } else if (position >= membersStartRow && position < membersEndRow) { + final TLRPC.ChatParticipant participant; + if (!sortedUsers.isEmpty()) { + participant = chatInfo.participants.participants.get(sortedUsers.get(position - membersStartRow)); + } else { + participant = chatInfo.participants.participants.get(position - membersStartRow); + } + return onMemberClick(participant, true); + } else { + return processOnClickOrPress(position); + } + } + }); + + if (searchItem != null) { + searchListView = new RecyclerListView(context); + searchListView.setVerticalScrollBarEnabled(false); + searchListView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); + searchListView.setGlowColor(Theme.getColor(Theme.key_avatar_backgroundActionBarBlue)); + searchListView.setAdapter(searchAdapter); + searchListView.setItemAnimator(null); + searchListView.setVisibility(View.GONE); + searchListView.setLayoutAnimation(null); + searchListView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + frameLayout.addView(searchListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); + searchListView.setOnItemClickListener((view, position) -> { + if (position < 0) { + return; + } + Object object = numberRow; + boolean add = true; + if (searchAdapter.searchWas) { + if (position < searchAdapter.searchResults.size()) { + object = searchAdapter.searchResults.get(position); + } else { + position -= searchAdapter.searchResults.size() + 1; + if (position >= 0 && position < searchAdapter.faqSearchResults.size()) { + object = searchAdapter.faqSearchResults.get(position); + } + } + } else { + if (!searchAdapter.recentSearches.isEmpty()) { + position--; + } + if (position >= 0 && position < searchAdapter.recentSearches.size()) { + object = searchAdapter.recentSearches.get(position); + } else { + position -= searchAdapter.recentSearches.size() + 1; + if (position >= 0 && position < searchAdapter.faqSearchArray.size()) { + object = searchAdapter.faqSearchArray.get(position); + add = false; } - getParentActivity().startActivityForResult(Intent.createChooser(intent, LocaleController.getString("BotShare", R.string.BotShare)), 500); - } catch (Exception e) { - FileLog.e(e); } } - } else if (position == locationRow) { - if (chatInfo.location instanceof TLRPC.TL_channelLocation) { - LocationActivity fragment = new LocationActivity(LocationActivity.LOCATION_TYPE_GROUP_VIEW); - fragment.setChatLocation(chat_id, (TLRPC.TL_channelLocation) chatInfo.location); - presentFragment(fragment); + if (object instanceof SearchAdapter.SearchResult) { + SearchAdapter.SearchResult result = (SearchAdapter.SearchResult) object; + result.open(); + } else if (object instanceof MessagesController.FaqSearchResult) { + MessagesController.FaqSearchResult result = (MessagesController.FaqSearchResult) object; + NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.openArticle, searchAdapter.faqWebPage, result.url); } - } else if (position == joinRow) { - MessagesController.getInstance(currentAccount).addUserToChat(currentChat.id, UserConfig.getInstance(currentAccount).getCurrentUser(), null, 0, null, ProfileActivity.this, null); - NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.closeSearchByActiveAction); - } else if (position == subscribersRow) { - Bundle args = new Bundle(); - args.putInt("chat_id", chat_id); - args.putInt("type", ChatUsersActivity.TYPE_USERS); - ChatUsersActivity fragment = new ChatUsersActivity(args); - fragment.setInfo(chatInfo); - presentFragment(fragment); - } else if (position == administratorsRow) { - Bundle args = new Bundle(); - args.putInt("chat_id", chat_id); - args.putInt("type", ChatUsersActivity.TYPE_ADMIN); - ChatUsersActivity fragment = new ChatUsersActivity(args); - fragment.setInfo(chatInfo); - presentFragment(fragment); - } else if (position == blockedUsersRow) { - Bundle args = new Bundle(); - args.putInt("chat_id", chat_id); - args.putInt("type", ChatUsersActivity.TYPE_BANNED); - ChatUsersActivity fragment = new ChatUsersActivity(args); - fragment.setInfo(chatInfo); - presentFragment(fragment); - } else { - processOnClickOrPress(position); - } - }); + if (add && object != null) { + searchAdapter.addRecent(object); + } + }); + searchListView.setOnItemLongClickListener((view, position) -> { + if (searchAdapter.isSearchWas() || searchAdapter.recentSearches.isEmpty()) { + return false; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setMessage(LocaleController.getString("ClearSearch", R.string.ClearSearch)); + builder.setPositiveButton(LocaleController.getString("ClearButton", R.string.ClearButton).toUpperCase(), (dialogInterface, i) -> searchAdapter.clearRecent()); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + return true; + }); + searchListView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { + AndroidUtilities.hideKeyboard(getParentActivity().getCurrentFocus()); + } + } + }); - listView.setOnItemLongClickListener((view, position) -> { - if (position >= membersStartRow && position < membersEndRow) { - final TLRPC.ChatParticipant participant; - if (!sortedUsers.isEmpty()) { - participant = chatInfo.participants.participants.get(sortedUsers.get(position - membersStartRow)); - } else { - participant = chatInfo.participants.participants.get(position - membersStartRow); - } - return onMemberClick(participant, true); - } else { - return processOnClickOrPress(position); - } - }); + emptyView = new EmptyTextProgressView(context); + emptyView.showTextView(); + emptyView.setTextSize(18); + emptyView.setVisibility(View.GONE); + emptyView.setShowAtCenter(true); + emptyView.setPadding(0, AndroidUtilities.dp(50), 0, 0); + emptyView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + frameLayout.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + searchAdapter.loadFaqWebPage(); + } if (banFromGroup != 0) { - TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(banFromGroup); + TLRPC.Chat chat = getMessagesController().getChat(banFromGroup); if (currentChannelParticipant == null) { TLRPC.TL_channels_getParticipant req = new TLRPC.TL_channels_getParticipant(); req.channel = MessagesController.getInputChannel(chat); - req.user_id = MessagesController.getInstance(currentAccount).getInputUser(user_id); - ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> { + req.user_id = getMessagesController().getInputUser(user_id); + getConnectionsManager().sendRequest(req, (response, error) -> { if (response != null) { AndroidUtilities.runOnUIThread(() -> currentChannelParticipant = ((TLRPC.TL_channels_channelParticipant) response).participant); } @@ -1940,40 +2708,64 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. topView.setBackgroundColor(Theme.getColor(Theme.key_avatar_backgroundActionBarBlue)); frameLayout.addView(topView); + avatarContainer = new FrameLayout(context); + avatarContainer.setPivotX(0); + avatarContainer.setPivotY(0); + frameLayout.addView(avatarContainer, LayoutHelper.createFrame(42, 42, Gravity.TOP | Gravity.LEFT, 64, 0, 0, 0)); + avatarImage = new AvatarImageView(context); avatarImage.setRoundRadius(AndroidUtilities.dp(21)); avatarImage.setPivotX(0); avatarImage.setPivotY(0); - frameLayout.addView(avatarImage, LayoutHelper.createFrame(42, 42, Gravity.TOP | Gravity.LEFT, 64, 0, 0, 0)); + avatarContainer.addView(avatarImage, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); avatarImage.setOnClickListener(v -> { - if (!isInLandscapeMode && avatarImage.getImageReceiver().hasNotThumb()) { - openingAvatar = true; - allowPullingDown = true; - View child = listView.getChildAt(0); - if (child != null) { - RecyclerView.ViewHolder holder = listView.findContainingViewHolder(child); - if (holder != null) { - Integer offset = positionToOffset.get(holder.getAdapterPosition()); - if (offset != null) { - listView.smoothScrollBy(0, -(offset + (listView.getPaddingTop() - child.getTop() - actionBar.getMeasuredHeight())), CubicBezierInterpolator.EASE_OUT_QUINT); - return; - } - } + if (imageUpdater != null) { + TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(UserConfig.getInstance(currentAccount).getClientUserId()); + if (user == null) { + user = UserConfig.getInstance(currentAccount).getCurrentUser(); } + if (user == null) { + return; + } + imageUpdater.openMenu(user.photo != null && user.photo.photo_big != null && !(user.photo instanceof TLRPC.TL_userProfilePhotoEmpty), () -> MessagesController.getInstance(currentAccount).deleteUserPhoto(null)); + } else { + if (avatarBig == null) { + return; + } + openAvatar(); } - openAvatar(); - }); - avatarImage.setOnLongClickListener(v -> { - openAvatar(); - return false; }); avatarImage.setContentDescription(LocaleController.getString("AccDescrProfilePicture", R.string.AccDescrProfilePicture)); + avatarProgressView = new RadialProgressView(context) { + private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + + { + paint.setColor(0x55000000); + } + + @Override + protected void onDraw(Canvas canvas) { + if (avatarImage != null && avatarImage.getImageReceiver().hasNotThumb()) { + paint.setAlpha((int) (0x55 * avatarImage.getImageReceiver().getCurrentAlpha())); + canvas.drawCircle(getMeasuredWidth() / 2.0f, getMeasuredHeight() / 2.0f, getMeasuredWidth() / 2.0f, paint); + } + super.onDraw(canvas); + } + }; + avatarProgressView.setSize(AndroidUtilities.dp(26)); + avatarProgressView.setProgressColor(0xffffffff); + avatarProgressView.setNoProgress(false); + avatarContainer.addView(avatarProgressView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + showAvatarProgress(false, false); + if (avatarsViewPager != null) { avatarsViewPager.onDestroy(); } overlaysView = new OverlaysView(context); avatarsViewPager = new ProfileGalleryView(context, user_id != 0 ? user_id : -chat_id, actionBar, listView, avatarImage, getClassGuid(), overlaysView); + avatarsViewPager.setChatInfo(chatInfo); frameLayout.addView(avatarsViewPager); frameLayout.addView(overlaysView); @@ -2001,6 +2793,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. nameTextView[a].setAlpha(a == 0 ? 0.0f : 1.0f); if (a == 1) { nameTextView[a].setScrollNonFitText(true); + nameTextView[a].setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); nameTextView[a].setOnLongClickListener(v -> { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setItems(new CharSequence[]{LocaleController.getString("Copy", R.string.Copy)}, (dialogInterface, i) -> { @@ -2029,6 +2822,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. onlineTextView[a].setTextSize(14); onlineTextView[a].setGravity(Gravity.LEFT); onlineTextView[a].setAlpha(a == 0 || a == 2 ? 0.0f : 1.0f); + if (a > 0) { + onlineTextView[a].setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } frameLayout.addView(onlineTextView[a], LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 118, 0, a == 0 ? 48 : 8, 0)); } @@ -2053,8 +2849,14 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. drawable = combinedDrawable; } writeButton.setBackgroundDrawable(drawable); - writeButton.setImageResource(R.drawable.profile_newmsg); - writeButton.setContentDescription(LocaleController.getString("AccDescrOpenChat", R.string.AccDescrOpenChat)); + if (imageUpdater != null) { + writeButton.setImageResource(R.drawable.baseline_edit_24); + writeButton.setContentDescription(LocaleController.getString("AccDescrChangeProfilePicture", R.string.AccDescrChangeProfilePicture)); + writeButton.setPadding(AndroidUtilities.dp(2), AndroidUtilities.dp(2), 0, 0); + } else { + writeButton.setImageResource(R.drawable.profile_newmsg); + writeButton.setContentDescription(LocaleController.getString("AccDescrOpenChat", R.string.AccDescrOpenChat)); + } writeButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_profile_actionIcon), PorterDuff.Mode.SRC_IN)); writeButton.setScaleType(ImageView.ScaleType.CENTER); if (Build.VERSION.SDK_INT >= 21) { @@ -2072,30 +2874,51 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } frameLayout.addView(writeButton, LayoutHelper.createFrame(Build.VERSION.SDK_INT >= 21 ? 56 : 60, Build.VERSION.SDK_INT >= 21 ? 56 : 60, Gravity.RIGHT | Gravity.TOP, 0, 0, 16, 0)); writeButton.setOnClickListener(v -> { - if (playProfileAnimation != 0 && parentLayout.fragmentsStack.get(parentLayout.fragmentsStack.size() - 2) instanceof ChatActivity && !AndroidUtilities.isTablet()) { - finishFragment(); + if (imageUpdater != null) { + presentFragment(new ChangeNameActivity()); } else { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); - if (user == null || user instanceof TLRPC.TL_userEmpty) { - return; - } - Bundle args = new Bundle(); - args.putInt("user_id", user_id); - if (!MessagesController.getInstance(currentAccount).checkCanOpenChat(args, ProfileActivity.this)) { - return; - } - if (!AndroidUtilities.isTablet()) { - NotificationCenter.getInstance(currentAccount).removeObserver(ProfileActivity.this, NotificationCenter.closeChats); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); - } - presentFragment(new ChatActivity(args), true); - if (AndroidUtilities.isTablet()) { + if (playProfileAnimation != 0 && parentLayout.fragmentsStack.get(parentLayout.fragmentsStack.size() - 2) instanceof ChatActivity && !AndroidUtilities.isTablet()) { finishFragment(); + } else { + TLRPC.User user = getMessagesController().getUser(user_id); + if (user == null || user instanceof TLRPC.TL_userEmpty) { + return; + } + Bundle args = new Bundle(); + args.putInt("user_id", user_id); + if (!getMessagesController().checkCanOpenChat(args, ProfileActivity.this)) { + return; + } + if (!AndroidUtilities.isTablet()) { + getNotificationCenter().removeObserver(ProfileActivity.this, NotificationCenter.closeChats); + getNotificationCenter().postNotificationName(NotificationCenter.closeChats); + } + int distance = getArguments().getInt("nearby_distance", -1); + if (distance >= 0) { + args.putInt("nearby_distance", distance); + } + ChatActivity chatActivity = new ChatActivity(args); + chatActivity.setPreloadedSticker(preloadedSticker); + presentFragment(chatActivity, true); + if (AndroidUtilities.isTablet()) { + finishFragment(); + } } } }); } - needLayout(); + needLayout(false); + + if (scrollTo != -1) { + layoutManager.scrollToPositionWithOffset(scrollTo, scrollToPosition); + + if (writeButtonTag != null) { + writeButton.setTag(0); + writeButton.setScaleX(0.2f); + writeButton.setScaleY(0.2f); + writeButton.setAlpha(0.0f); + } + } listView.setOnScrollListener(new RecyclerView.OnScrollListener() { @@ -2107,6 +2930,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (openingAvatar && newState != RecyclerView.SCROLL_STATE_SETTLING) { openingAvatar = false; } + if (searchItem != null) { + scrolling = newState != RecyclerView.SCROLL_STATE_IDLE; + searchItem.setEnabled(!scrolling && !isPulledDown); + } } @Override @@ -2126,11 +2953,14 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. final int newTop = ActionBar.getCurrentActionBarHeight() + (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0); final float value = AndroidUtilities.lerp(expandAnimatorValues, currentExpanAnimatorFracture = anim.getAnimatedFraction()); - avatarImage.setScaleX(avatarScale); - avatarImage.setScaleY(avatarScale); - avatarImage.setTranslationX(AndroidUtilities.lerp(avatarX, 0f, value)); - avatarImage.setTranslationY(AndroidUtilities.lerp((float) Math.ceil(avatarY), 0f, value)); + avatarContainer.setScaleX(avatarScale); + avatarContainer.setScaleY(avatarScale); + avatarContainer.setTranslationX(AndroidUtilities.lerp(avatarX, 0f, value)); + avatarContainer.setTranslationY(AndroidUtilities.lerp((float) Math.ceil(avatarY), 0f, value)); avatarImage.setRoundRadius((int) AndroidUtilities.lerp(AndroidUtilities.dpf2(21f), 0f, value)); + if (searchItem != null) { + searchItem.setAlpha(1.0f - value); + } if (extraHeight > AndroidUtilities.dp(88f) && expandProgress < 0.33f) { refreshNameAndOnlineXY(); @@ -2201,11 +3031,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. avatarImage.setForegroundAlpha(value); - final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) avatarImage.getLayoutParams(); + final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) avatarContainer.getLayoutParams(); params.width = (int) AndroidUtilities.lerp(AndroidUtilities.dpf2(42f), listView.getMeasuredWidth() / avatarScale, value); params.height = (int) AndroidUtilities.lerp(AndroidUtilities.dpf2(42f), (extraHeight + newTop) / avatarScale, value); params.leftMargin = (int) AndroidUtilities.lerp(AndroidUtilities.dpf2(64f), 0f, value); - avatarImage.requestLayout(); + avatarContainer.requestLayout(); }); expandAnimator.setInterpolator(CubicBezierInterpolator.EASE_BOTH); expandAnimator.addListener(new AnimatorListenerAdapter() { @@ -2217,6 +3047,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override public void onAnimationEnd(Animator animation) { actionBar.setItemsBackgroundColor(isPulledDown ? Theme.ACTION_BAR_WHITE_SELECTOR_COLOR : Theme.getColor(Theme.key_avatar_actionBarSelectorBlue), false); + avatarImage.clearForeground(); + doNotSetForeground = false; } }); @@ -2244,7 +3076,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return; } if (user_id != 0) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + TLRPC.User user = getMessagesController().getUser(user_id); if (user.photo != null && user.photo.photo_big != null) { PhotoViewer.getInstance().setParentActivity(getParentActivity()); if (user.photo.dc_id != 0) { @@ -2253,13 +3085,19 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. PhotoViewer.getInstance().openPhoto(user.photo.photo_big, provider); } } else if (chat_id != 0) { - TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(chat_id); + TLRPC.Chat chat = getMessagesController().getChat(chat_id); if (chat.photo != null && chat.photo.photo_big != null) { PhotoViewer.getInstance().setParentActivity(getParentActivity()); if (chat.photo.dc_id != 0) { chat.photo.photo_big.dc_id = chat.photo.dc_id; } - PhotoViewer.getInstance().openPhoto(chat.photo.photo_big, provider); + ImageLocation videoLocation; + if (chatInfo != null && (chatInfo.chat_photo instanceof TLRPC.TL_photo) && !chatInfo.chat_photo.video_sizes.isEmpty()) { + videoLocation = ImageLocation.getForPhoto(chatInfo.chat_photo.video_sizes.get(0), chatInfo.chat_photo); + } else { + videoLocation = null; + } + PhotoViewer.getInstance().openPhotoWithVideo(chat.photo.photo_big, videoLocation, provider); } } } @@ -2273,8 +3111,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return false; } if (isLong) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(participant.user_id); - if (user == null || participant.user_id == UserConfig.getInstance(currentAccount).getClientUserId()) { + TLRPC.User user = getMessagesController().getUser(participant.user_id); + if (user == null || participant.user_id == getUserConfig().getClientUserId()) { return false; } selectedUser = participant.user_id; @@ -2286,7 +3124,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (ChatObject.isChannel(currentChat)) { channelParticipant = ((TLRPC.TL_chatChannelParticipant) participant).channelParticipant; - TLRPC.User u = MessagesController.getInstance(currentAccount).getUser(participant.user_id); + TLRPC.User u = getMessagesController().getUser(participant.user_id); canEditAdmin = ChatObject.canAddAdmins(currentChat); if (canEditAdmin && (channelParticipant instanceof TLRPC.TL_channelParticipantCreator || channelParticipant instanceof TLRPC.TL_channelParticipantAdmin && !channelParticipant.can_edit)) { canEditAdmin = false; @@ -2295,7 +3133,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. editingAdmin = channelParticipant instanceof TLRPC.TL_channelParticipantAdmin; } else { channelParticipant = null; - allowKick = currentChat.creator || participant instanceof TLRPC.TL_chatParticipant && (ChatObject.canBlockUsers(currentChat) || participant.inviter_id == UserConfig.getInstance(currentAccount).getClientUserId()); + allowKick = currentChat.creator || participant instanceof TLRPC.TL_chatParticipant && (ChatObject.canBlockUsers(currentChat) || participant.inviter_id == getUserConfig().getClientUserId()); canEditAdmin = currentChat.creator; canRestrict = currentChat.creator; editingAdmin = participant instanceof TLRPC.TL_chatParticipantAdmin; @@ -2372,7 +3210,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. alertDialog.setItemColor(items.size() - 1, Theme.getColor(Theme.key_dialogTextRed2), Theme.getColor(Theme.key_dialogRedIcon)); } } else { - if (participant.user_id == UserConfig.getInstance(currentAccount).getClientUserId()) { + if (participant.user_id == getUserConfig().getClientUserId()) { return false; } Bundle args = new Bundle(); @@ -2396,7 +3234,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else { channelParticipant1.channelParticipant = new TLRPC.TL_channelParticipant(); } - channelParticipant1.channelParticipant.inviter_id = UserConfig.getInstance(currentAccount).getClientUserId(); + channelParticipant1.channelParticipant.inviter_id = getUserConfig().getClientUserId(); channelParticipant1.channelParticipant.user_id = participant.user_id; channelParticipant1.channelParticipant.date = participant.date; channelParticipant1.channelParticipant.banned_rights = rightsBanned; @@ -2464,13 +3302,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (position == usernameRow) { final String username; if (user_id != 0) { - final TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + final TLRPC.User user = getMessagesController().getUser(user_id); if (user == null || user.username == null) { return false; } username = user.username; } else if (chat_id != 0) { - final TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(chat_id); + final TLRPC.Chat chat = getMessagesController().getChat(chat_id); if (chat == null || chat.username == null) { return false; } @@ -2494,7 +3332,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. showDialog(builder.create()); return true; } else if (position == phoneRow) { - final TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + final TLRPC.User user = getMessagesController().getUser(user_id); if (user == null || user.phone == null || user.phone.length() == 0 || getParentActivity() == null || (NekoConfig.hidePhone && user.id == UserConfig.getInstance(currentAccount).getClientUserId())) { return false; @@ -2566,10 +3404,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private void leaveChatPressed() { AlertsCreator.createClearOrDeleteDialogAlert(ProfileActivity.this, false, currentChat, null, false, (param) -> { playProfileAnimation = 0; - NotificationCenter.getInstance(currentAccount).removeObserver(ProfileActivity.this, NotificationCenter.closeChats); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); + getNotificationCenter().removeObserver(ProfileActivity.this, NotificationCenter.closeChats); + getNotificationCenter().postNotificationName(NotificationCenter.closeChats); finishFragment(); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.needDeleteDialog, (long) -currentChat.id, null, currentChat, param); + getNotificationCenter().postNotificationName(NotificationCenter.needDeleteDialog, (long) -currentChat.id, null, currentChat, param); }); } @@ -2581,22 +3419,22 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. final int delay = participantsMap.size() != 0 && reload ? 300 : 0; final TLRPC.TL_channels_getParticipants req = new TLRPC.TL_channels_getParticipants(); - req.channel = MessagesController.getInstance(currentAccount).getInputChannel(chat_id); + req.channel = getMessagesController().getInputChannel(chat_id); req.filter = new TLRPC.TL_channelParticipantsRecent(); req.offset = reload ? 0 : participantsMap.size(); req.limit = 200; - int reqId = ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> { + int reqId = getConnectionsManager().sendRequest(req, (response, error) -> AndroidUtilities.runOnUIThread(() -> { if (error == null) { TLRPC.TL_channels_channelParticipants res = (TLRPC.TL_channels_channelParticipants) response; - MessagesController.getInstance(currentAccount).putUsers(res.users, false); + getMessagesController().putUsers(res.users, false); if (res.users.size() < 200) { usersEndReached = true; } if (req.offset == 0) { participantsMap.clear(); chatInfo.participants = new TLRPC.TL_chatParticipants(); - MessagesStorage.getInstance(currentAccount).putUsersAndChats(res.users, null, true, true); - MessagesStorage.getInstance(currentAccount).updateChannelUsers(chat_id, res.participants); + getMessagesStorage().putUsersAndChats(res.users, null, true, true); + getMessagesStorage().updateChannelUsers(chat_id, res.participants); } for (int a = 0; a < res.participants.size(); a++) { TLRPC.TL_chatChannelParticipant participant = new TLRPC.TL_chatChannelParticipant(); @@ -2605,6 +3443,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. participant.user_id = participant.channelParticipant.user_id; participant.date = participant.channelParticipant.date; if (participantsMap.indexOfKey(participant.user_id) < 0) { + if (chatInfo.participants == null) { + chatInfo.participants = new TLRPC.TL_chatParticipants(); + } chatInfo.participants.participants.add(participant); participantsMap.put(participant.user_id, participant); } @@ -2617,7 +3458,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. listAdapter.notifyDataSetChanged(); } }, delay)); - ConnectionsManager.getInstance(currentAccount).bindRequestToGuid(reqId, classGuid); + getConnectionsManager().bindRequestToGuid(reqId, classGuid); } private AnimatorSet headerAnimatorSet; @@ -2685,7 +3526,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (headerShadowAnimatorSet != null) { headerShadowAnimatorSet.cancel(); } - ActionBarMenuItem searchItem = sharedMediaLayout.getSearchItem(); + ActionBarMenuItem mediaSearchItem = sharedMediaLayout.getSearchItem(); if (!mediaHeaderVisible) { if (callItemVisible) { callItem.setVisibility(View.VISIBLE); @@ -2696,7 +3537,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. otherItem.setVisibility(View.VISIBLE); } else { if (sharedMediaLayout.isSearchItemVisible()) { - searchItem.setVisibility(View.VISIBLE); + mediaSearchItem.setVisibility(View.VISIBLE); } } @@ -2708,8 +3549,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. animators.add(ObjectAnimator.ofFloat(callItem, View.TRANSLATION_Y, visible ? -AndroidUtilities.dp(10) : 0.0f)); animators.add(ObjectAnimator.ofFloat(otherItem, View.TRANSLATION_Y, visible ? -AndroidUtilities.dp(10) : 0.0f)); animators.add(ObjectAnimator.ofFloat(editItem, View.TRANSLATION_Y, visible ? -AndroidUtilities.dp(10) : 0.0f)); - animators.add(ObjectAnimator.ofFloat(searchItem, View.ALPHA, visible ? 1.0f : 0.0f)); - animators.add(ObjectAnimator.ofFloat(searchItem, View.TRANSLATION_Y, visible ? 0.0f : AndroidUtilities.dp(10))); + animators.add(ObjectAnimator.ofFloat(mediaSearchItem, View.ALPHA, visible ? 1.0f : 0.0f)); + animators.add(ObjectAnimator.ofFloat(mediaSearchItem, View.TRANSLATION_Y, visible ? 0.0f : AndroidUtilities.dp(10))); animators.add(ObjectAnimator.ofFloat(actionBar, ACTIONBAR_HEADER_PROGRESS, visible ? 1.0f : 0.0f)); animators.add(ObjectAnimator.ofFloat(onlineTextView[1], View.ALPHA, visible ? 0.0f : 1.0f)); animators.add(ObjectAnimator.ofFloat(onlineTextView[2], View.ALPHA, visible ? 1.0f : 0.0f)); @@ -2734,7 +3575,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. otherItem.setVisibility(View.INVISIBLE); } else { if (sharedMediaLayout.isSearchItemVisible()) { - searchItem.setVisibility(View.VISIBLE); + mediaSearchItem.setVisibility(View.VISIBLE); } headerShadowAnimatorSet = new AnimatorSet(); headerShadowAnimatorSet.playTogether(ObjectAnimator.ofFloat(ProfileActivity.this, HEADER_SHADOW, 1.0f)); @@ -2776,13 +3617,16 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. fragment.setDelegate((users, fwdCount) -> { for (int a = 0, N = users.size(); a < N; a++) { TLRPC.User user = users.get(a); - MessagesController.getInstance(currentAccount).addUserToChat(chat_id, user, chatInfo, fwdCount, null, ProfileActivity.this, null); + getMessagesController().addUserToChat(chat_id, user, chatInfo, fwdCount, null, ProfileActivity.this, null); } }); presentFragment(fragment); } private void checkListViewScroll() { + if (listView.getVisibility() != View.VISIBLE) { + return; + } if (sharedMediaLayoutAttached) { sharedMediaLayout.setVisibleHeight(listView.getMeasuredHeight() - sharedMediaLayout.getTop()); } @@ -2800,7 +3644,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. newOffset = top; } boolean mediaHeaderVisible; - boolean searchVisible = actionBar.isSearchFieldVisible(); + boolean searchVisible = imageUpdater == null && actionBar.isSearchFieldVisible(); if (sharedMediaRow != -1 && !searchVisible) { holder = (RecyclerListView.Holder) listView.findViewHolderForAdapterPosition(sharedMediaRow); mediaHeaderVisible = holder != null && holder.itemView.getTop() <= 0; @@ -2815,7 +3659,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (playProfileAnimation != 0) { allowProfileAnimation = extraHeight != 0; } - needLayout(); + needLayout(true); } } @@ -2844,7 +3688,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } - private void needLayout() { + private void needLayout(boolean animated) { final int newTop = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); FrameLayout.LayoutParams layoutParams; @@ -2856,7 +3700,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } - if (avatarImage != null) { + if (avatarContainer != null) { final float diff = Math.min(1f, extraHeight / AndroidUtilities.dp(88f)); listView.setTopGlowOffset((int) extraHeight); @@ -2864,10 +3708,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. listView.setOverScrollMode(extraHeight > AndroidUtilities.dp(88f) && extraHeight < listView.getMeasuredWidth() - newTop ? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_ALWAYS); if (writeButton != null) { - writeButton.setTranslationY((actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); + writeButton.setTranslationY((actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight + searchTransitionOffset - AndroidUtilities.dp(29.5f)); if (!openAnimationInProgress) { - final boolean setVisible = diff > 0.2f; + boolean setVisible = diff > 0.2f && !searchMode && (imageUpdater == null || setAvatarRow == -1); boolean currentVisible = writeButton.getTag() == null; if (setVisible != currentVisible) { if (setVisible) { @@ -2880,32 +3724,38 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. writeButtonAnimation = null; old.cancel(); } - writeButtonAnimation = new AnimatorSet(); - if (setVisible) { - writeButtonAnimation.setInterpolator(new DecelerateInterpolator()); - writeButtonAnimation.playTogether( - ObjectAnimator.ofFloat(writeButton, View.SCALE_X, 1.0f), - ObjectAnimator.ofFloat(writeButton, View.SCALE_Y, 1.0f), - ObjectAnimator.ofFloat(writeButton, View.ALPHA, 1.0f) - ); - } else { - writeButtonAnimation.setInterpolator(new AccelerateInterpolator()); - writeButtonAnimation.playTogether( - ObjectAnimator.ofFloat(writeButton, View.SCALE_X, 0.2f), - ObjectAnimator.ofFloat(writeButton, View.SCALE_Y, 0.2f), - ObjectAnimator.ofFloat(writeButton, View.ALPHA, 0.0f) - ); - } - writeButtonAnimation.setDuration(150); - writeButtonAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (writeButtonAnimation != null && writeButtonAnimation.equals(animation)) { - writeButtonAnimation = null; - } + if (animated) { + writeButtonAnimation = new AnimatorSet(); + if (setVisible) { + writeButtonAnimation.setInterpolator(new DecelerateInterpolator()); + writeButtonAnimation.playTogether( + ObjectAnimator.ofFloat(writeButton, View.SCALE_X, 1.0f), + ObjectAnimator.ofFloat(writeButton, View.SCALE_Y, 1.0f), + ObjectAnimator.ofFloat(writeButton, View.ALPHA, 1.0f) + ); + } else { + writeButtonAnimation.setInterpolator(new AccelerateInterpolator()); + writeButtonAnimation.playTogether( + ObjectAnimator.ofFloat(writeButton, View.SCALE_X, 0.2f), + ObjectAnimator.ofFloat(writeButton, View.SCALE_Y, 0.2f), + ObjectAnimator.ofFloat(writeButton, View.ALPHA, 0.0f) + ); } - }); - writeButtonAnimation.start(); + writeButtonAnimation.setDuration(150); + writeButtonAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (writeButtonAnimation != null && writeButtonAnimation.equals(animation)) { + writeButtonAnimation = null; + } + } + }); + writeButtonAnimation.start(); + } else { + writeButton.setScaleX(setVisible ? 1.0f : 0.2f); + writeButton.setScaleY(setVisible ? 1.0f : 0.2f); + writeButton.setAlpha(setVisible ? 1.0f : 0.0f); + } } } } @@ -2920,10 +3770,21 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. final float durationFactor = Math.min(AndroidUtilities.dpf2(2000f), Math.max(AndroidUtilities.dpf2(1100f), Math.abs(listViewVelocityY))) / AndroidUtilities.dpf2(1100f); - if (openingAvatar || expandProgress >= 0.33f) { + if (allowPullingDown && (openingAvatar || expandProgress >= 0.33f)) { if (!isPulledDown) { if (otherItem != null) { otherItem.showSubItem(gallery_menu_save); + if (imageUpdater != null) { + otherItem.showSubItem(add_photo); + otherItem.showSubItem(edit_avatar); + otherItem.showSubItem(delete_avatar); + otherItem.hideSubItem(set_as_main); + otherItem.hideSubItem(logout); + otherItem.hideSubItem(edit_name); + } + } + if (searchItem != null) { + searchItem.setEnabled(false); } isPulledDown = true; overlaysView.setOverlaysVisible(true, durationFactor); @@ -2936,16 +3797,16 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. expandAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { - avatarImage.setForegroundImage(avatarsViewPager.getImageLocation(0), null, avatarImage.getImageReceiver().getDrawable()); + setForegroundImage(false); + avatarsViewPager.setAnimatedFileMaybe(avatarImage.getImageReceiver().getAnimation()); avatarsViewPager.resetCurrentItem(); } @Override public void onAnimationEnd(Animator animation) { expandAnimator.removeListener(this); - avatarImage.clearForeground(); topView.setBackgroundColor(Color.BLACK); - avatarImage.setVisibility(View.GONE); + avatarContainer.setVisibility(View.GONE); avatarsViewPager.setVisibility(View.VISIBLE); } }); @@ -2974,10 +3835,23 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. isPulledDown = false; if (otherItem != null) { otherItem.hideSubItem(gallery_menu_save); + if (imageUpdater != null) { + otherItem.hideSubItem(set_as_main); + otherItem.hideSubItem(edit_avatar); + otherItem.hideSubItem(delete_avatar); + otherItem.hideSubItem(add_photo); + otherItem.showSubItem(logout); + otherItem.showSubItem(edit_name); + } + } + if (searchItem != null) { + searchItem.setEnabled(!scrolling); } overlaysView.setOverlaysVisible(false, durationFactor); avatarsViewPagerIndicatorView.refreshVisibility(durationFactor); expandAnimator.cancel(); + avatarImage.getImageReceiver().setAllowStartAnimation(true); + avatarImage.getImageReceiver().startAnimation(); float value = AndroidUtilities.lerp(expandAnimatorValues, currentExpanAnimatorFracture); expandAnimatorValues[0] = value; @@ -2989,18 +3863,20 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } topView.setBackgroundColor(Theme.getColor(Theme.key_avatar_backgroundActionBarBlue)); - BackupImageView imageView = avatarsViewPager.getCurrentItemView(); - if (imageView != null) { - avatarImage.setForegroundImageDrawable(imageView.getImageReceiver().getDrawable()); + if (!doNotSetForeground) { + BackupImageView imageView = avatarsViewPager.getCurrentItemView(); + if (imageView != null) { + avatarImage.setForegroundImageDrawable(imageView.getImageReceiver().getDrawableSafe()); + } } avatarImage.setForegroundAlpha(1f); - avatarImage.setVisibility(View.VISIBLE); + avatarContainer.setVisibility(View.VISIBLE); avatarsViewPager.setVisibility(View.GONE); expandAnimator.start(); } - avatarImage.setScaleX(avatarScale); - avatarImage.setScaleY(avatarScale); + avatarContainer.setScaleX(avatarScale); + avatarContainer.setScaleY(avatarScale); if (expandAnimator == null || !expandAnimator.isRunning()) { refreshNameAndOnlineXY(); @@ -3034,10 +3910,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. avatarScale = AndroidUtilities.lerp(1.0f, (42f + 42f + 18f) / 42f, animationProgress); avatarImage.setRoundRadius((int) AndroidUtilities.lerp(AndroidUtilities.dpf2(21f), 0f, animationProgress)); - avatarImage.setTranslationX(AndroidUtilities.lerp(avX, 0, animationProgress)); - avatarImage.setTranslationY(AndroidUtilities.lerp((float) Math.ceil(avY), 0f, animationProgress)); - avatarImage.setScaleX(avatarScale); - avatarImage.setScaleY(avatarScale); + avatarContainer.setTranslationX(AndroidUtilities.lerp(avX, 0, animationProgress)); + avatarContainer.setTranslationY(AndroidUtilities.lerp((float) Math.ceil(avY), 0f, animationProgress)); + avatarContainer.setScaleX(avatarScale); + avatarContainer.setScaleY(avatarScale); overlaysView.setAlphaValue(animationProgress, false); actionBar.setItemsColor(ColorUtils.blendARGB(Theme.getColor(Theme.key_actionBarDefaultIcon), Color.WHITE, animationProgress), false); @@ -3053,18 +3929,18 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. nameTextView[1].invalidate(); } - final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) avatarImage.getLayoutParams(); + final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) avatarContainer.getLayoutParams(); params.width = params.height = (int) AndroidUtilities.lerp(AndroidUtilities.dpf2(42f), (extraHeight + newTop) / avatarScale, animationProgress); params.leftMargin = (int) AndroidUtilities.lerp(AndroidUtilities.dpf2(64f), 0f, animationProgress); - avatarImage.requestLayout(); + avatarContainer.requestLayout(); } else if (extraHeight <= AndroidUtilities.dp(88f)) { avatarScale = (42 + 18 * diff) / 42.0f; float nameScale = 1.0f + 0.12f * diff; if (expandAnimator == null || !expandAnimator.isRunning()) { - avatarImage.setScaleX(avatarScale); - avatarImage.setScaleY(avatarScale); - avatarImage.setTranslationX(avatarX); - avatarImage.setTranslationY((float) Math.ceil(avatarY)); + avatarContainer.setScaleX(avatarScale); + avatarContainer.setScaleY(avatarScale); + avatarContainer.setTranslationX(avatarX); + avatarContainer.setTranslationY((float) Math.ceil(avatarY)); } nameX = -21 * AndroidUtilities.density * diff; nameY = (float) Math.floor(avatarY) + AndroidUtilities.dp(1.3f) + AndroidUtilities.dp(7) * diff; @@ -3113,14 +3989,33 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } + private void setForegroundImage(boolean secondParent) { + Drawable drawable = avatarImage.getImageReceiver().getDrawable(); + if (drawable instanceof AnimatedFileDrawable) { + AnimatedFileDrawable fileDrawable = (AnimatedFileDrawable) drawable; + avatarImage.setForegroundImage(null, null, fileDrawable); + if (secondParent) { + fileDrawable.addSecondParentView(avatarImage); + } + } else { + ImageLocation location = avatarsViewPager.getImageLocation(0); + String filter; + if (location != null && location.imageType == FileLoader.IMAGE_TYPE_ANIMATION) { + filter = ImageLoader.AUTOPLAY_FILTER; + } else { + filter = null; + } + avatarImage.setForegroundImage(location, filter, drawable); + } + } + private void refreshNameAndOnlineXY() { - nameX = AndroidUtilities.dp(-21f) + avatarImage.getMeasuredWidth() * (avatarScale - (42f + 18f) / 42f); - nameY = (float) Math.floor(avatarY) + AndroidUtilities.dp(1.3f) + AndroidUtilities.dp(7f) + avatarImage.getMeasuredHeight() * (avatarScale - (42f + 18f) / 42f) / 2f; - onlineX = AndroidUtilities.dp(-21f) + avatarImage.getMeasuredWidth() * (avatarScale - (42f + 18f) / 42f); - onlineY = (float) Math.floor(avatarY) + AndroidUtilities.dp(24) + (float) Math.floor(11 * AndroidUtilities.density) + avatarImage.getMeasuredHeight() * (avatarScale - (42f + 18f) / 42f) / 2f; + nameX = AndroidUtilities.dp(-21f) + avatarContainer.getMeasuredWidth() * (avatarScale - (42f + 18f) / 42f); + nameY = (float) Math.floor(avatarY) + AndroidUtilities.dp(1.3f) + AndroidUtilities.dp(7f) + avatarContainer.getMeasuredHeight() * (avatarScale - (42f + 18f) / 42f) / 2f; + onlineX = AndroidUtilities.dp(-21f) + avatarContainer.getMeasuredWidth() * (avatarScale - (42f + 18f) / 42f); + onlineY = (float) Math.floor(avatarY) + AndroidUtilities.dp(24) + (float) Math.floor(11 * AndroidUtilities.density) + avatarContainer.getMeasuredHeight() * (avatarScale - (42f + 18f) / 42f) / 2f; idX = AndroidUtilities.dp(-21f) + avatarImage.getMeasuredWidth() * (avatarScale - (42f + 18f) / 42f); idY = (float) Math.floor(avatarY) + AndroidUtilities.dp(32) + (float) Math.floor(22 * AndroidUtilities.density) + avatarImage.getMeasuredHeight() * (avatarScale - (42f + 18f) / 42f) / 2f; - } public RecyclerListView getListView() { @@ -3137,7 +4032,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } int viewWidth = AndroidUtilities.isTablet() ? AndroidUtilities.dp(490) : AndroidUtilities.displaySize.x; - int buttonsWidth = AndroidUtilities.dp(118 + 8 + (40 + (callItemVisible || editItemVisible ? 48 : 0))); + int buttonsWidth = AndroidUtilities.dp(118 + 8 + (40 + (callItemVisible || editItemVisible || searchItem != null ? 48 : 0))); int minWidth = viewWidth - buttonsWidth; int width = (int) (viewWidth - buttonsWidth * Math.max(0.0f, 1.0f - (diff != 1.0f ? diff * 0.15f / (1.0f - diff) : 1.0f)) - nameTextView[1].getTranslationX()); @@ -3192,7 +4087,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public boolean onPreDraw() { if (fragmentView != null) { checkListViewScroll(); - needLayout(); + needLayout(true); fragmentView.getViewTreeObserver().removeOnPreDrawListener(this); } return true; @@ -3277,8 +4172,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else if (id == NotificationCenter.encryptedChatCreated) { if (creatingChat) { AndroidUtilities.runOnUIThread(() -> { - NotificationCenter.getInstance(currentAccount).removeObserver(ProfileActivity.this, NotificationCenter.closeChats); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); + getNotificationCenter().removeObserver(ProfileActivity.this, NotificationCenter.closeChats); + getNotificationCenter().postNotificationName(NotificationCenter.closeChats); TLRPC.EncryptedChat encryptedChat = (TLRPC.EncryptedChat) args[0]; Bundle args2 = new Bundle(); args2.putInt("enc_id", encryptedChat.id); @@ -3298,11 +4193,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } else if (id == NotificationCenter.blockedUsersDidLoad) { boolean oldValue = userBlocked; - userBlocked = MessagesController.getInstance(currentAccount).blockedUsers.indexOfKey(user_id) >= 0; + userBlocked = getMessagesController().blockedUsers.indexOfKey(user_id) >= 0; if (oldValue != userBlocked) { createActionBarMenu(); updateRowsIds(); - listAdapter.notifyDataSetChanged(); + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } } } else if (id == NotificationCenter.chatInfoDidLoad) { TLRPC.ChatFull chatFull = (TLRPC.ChatFull) args[0]; @@ -3317,15 +4214,18 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. chatInfo = chatFull; if (mergeDialogId == 0 && chatInfo.migrated_from_chat_id != 0) { mergeDialogId = -chatInfo.migrated_from_chat_id; - MediaDataController.getInstance(currentAccount).getMediaCount(mergeDialogId, MediaDataController.MEDIA_PHOTOVIDEO, classGuid, true); + getMediaDataController().getMediaCount(mergeDialogId, MediaDataController.MEDIA_PHOTOVIDEO, classGuid, true); } fetchUsersFromChannelInfo(); updateOnlineCount(); updateRowsIds(); + if (avatarsViewPager != null) { + avatarsViewPager.setChatInfo(chatInfo); + } if (listAdapter != null) { listAdapter.notifyDataSetChanged(); } - TLRPC.Chat newChat = MessagesController.getInstance(currentAccount).getChat(chat_id); + TLRPC.Chat newChat = getMessagesController().getChat(chat_id); if (newChat != null) { currentChat = newChat; createActionBarMenu(); @@ -3349,23 +4249,29 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. int uid = (Integer) args[0]; if (uid == user_id) { userInfo = (TLRPC.UserFull) args[1]; - if (!openAnimationInProgress && !callItemVisible) { - createActionBarMenu(); - } else { - recreateMenuAfterAnimation = true; - } - updateRowsIds(); - if (listAdapter != null) { - try { - listAdapter.notifyDataSetChanged(); - } catch (Exception e) { - FileLog.e(e); + if (imageUpdater != null) { + if (!TextUtils.equals(userInfo.about, currentBio)) { + listAdapter.notifyItemChanged(bioRow); } + } else { + if (!openAnimationInProgress && !callItemVisible) { + createActionBarMenu(); + } else { + recreateMenuAfterAnimation = true; + } + updateRowsIds(); + if (listAdapter != null) { + try { + listAdapter.notifyDataSetChanged(); + } catch (Exception e) { + FileLog.e(e); + } + } + if (sharedMediaLayout != null) { + sharedMediaLayout.setCommonGroupsCount(userInfo.common_chats_count); + } + updateSelectedMediaTabText(); } - if (sharedMediaLayout != null) { - sharedMediaLayout.setCommonGroupsCount(userInfo.common_chats_count); - } - updateSelectedMediaTabText(); } } else if (id == NotificationCenter.didReceiveNewMessages) { boolean scheduled = (Boolean) args[2]; @@ -3412,6 +4318,14 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (listAdapter != null) { listAdapter.notifyDataSetChanged(); } + + if (imageUpdater != null) { + AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid, true); + AndroidUtilities.removeAdjustResize(getParentActivity(), classGuid, true); + imageUpdater.onResume(); + setParentActivityTitle(LocaleController.getString("Settings", R.string.Settings)); + } + updateProfileData(); fixLayout(); if (nameTextView[1] != null) { @@ -3425,6 +4339,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (undoView != null) { undoView.hide(true, 0); } + if (imageUpdater != null) { + imageUpdater.onPause(); + } } @Override @@ -3443,6 +4360,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return actionBar.isEnabled() && (sharedMediaRow == -1 || sharedMediaLayout == null || !sharedMediaLayout.closeActionMode()); } + public boolean isSettings() { + return imageUpdater != null; + } + @Override protected void onBecomeFullyHidden() { if (undoView != null) { @@ -3478,8 +4399,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. openAnimationInProgress = true; } if (isOpen) { - transitionIndex = NotificationCenter.getInstance(currentAccount).setAnimationInProgress(transitionIndex, new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoad, NotificationCenter.mediaCountsDidLoad}); + if (imageUpdater != null) { + transitionIndex = getNotificationCenter().setAnimationInProgress(transitionIndex, new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoad, NotificationCenter.mediaCountsDidLoad, NotificationCenter.userInfoDidLoad}); + } else { + transitionIndex = getNotificationCenter().setAnimationInProgress(transitionIndex, new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoad, NotificationCenter.mediaCountsDidLoad}); + } } + transitionAnimationInProress = true; } @Override @@ -3493,8 +4419,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } } - NotificationCenter.getInstance(currentAccount).onAnimationFinish(transitionIndex); + getNotificationCenter().onAnimationFinish(transitionIndex); } + transitionAnimationInProress = false; } @Keep @@ -3588,7 +4515,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. topView.invalidate(); - needLayout(); + needLayout(true); + fragmentView.invalidate(); } @Override @@ -3641,9 +4569,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. avatarColor = AndroidUtilities.calcBitmapColor(avatarImage.getImageReceiver().getBitmap()); nameTextView[1].setTextColor(Color.WHITE); onlineTextView[1].setTextColor(Color.argb(179, 255, 255, 255)); + idTextView.setAlpha(0); idTextView.setTextColor(Color.argb(179, 255, 255, 255)); actionBar.setItemsBackgroundColor(Theme.ACTION_BAR_WHITE_SELECTOR_COLOR, false); overlaysView.setOverlaysVisible(); + animators.add(ObjectAnimator.ofFloat(idTextView, View.ALPHA, 0.0f, 1.0f)); } for (int a = 0; a < 2; a++) { onlineTextView[a].setAlpha(a == 0 ? 1.0f : 0.0f); @@ -3704,9 +4634,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (playProfileAnimation == 2) { playProfileAnimation = 1; avatarImage.setForegroundAlpha(1.0f); - avatarImage.setVisibility(View.GONE); + avatarContainer.setVisibility(View.GONE); avatarsViewPager.resetCurrentItem(); avatarsViewPager.setVisibility(View.VISIBLE); + idTextView.setAlpha(1.0f); } } }); @@ -3720,13 +4651,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private void updateOnlineCount() { onlineCount = 0; - int currentTime = ConnectionsManager.getInstance(currentAccount).getCurrentTime(); + int currentTime = getConnectionsManager().getCurrentTime(); sortedUsers.clear(); if (chatInfo instanceof TLRPC.TL_chatFull || chatInfo instanceof TLRPC.TL_channelFull && chatInfo.participants_count <= 200 && chatInfo.participants != null) { for (int a = 0; a < chatInfo.participants.participants.size(); a++) { TLRPC.ChatParticipant participant = chatInfo.participants.participants.get(a); - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(participant.user_id); - if (user != null && user.status != null && (user.status.expires > currentTime || user.id == UserConfig.getInstance(currentAccount).getClientUserId()) && user.status.expires > 10000) { + TLRPC.User user = getMessagesController().getUser(participant.user_id); + if (user != null && user.status != null && (user.status.expires > currentTime || user.id == getUserConfig().getClientUserId()) && user.status.expires > 10000) { onlineCount++; } sortedUsers.add(a); @@ -3734,8 +4665,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. try { Collections.sort(sortedUsers, (lhs, rhs) -> { - TLRPC.User user1 = MessagesController.getInstance(currentAccount).getUser(chatInfo.participants.participants.get(rhs).user_id); - TLRPC.User user2 = MessagesController.getInstance(currentAccount).getUser(chatInfo.participants.participants.get(lhs).user_id); + TLRPC.User user1 = getMessagesController().getUser(chatInfo.participants.participants.get(rhs).user_id); + TLRPC.User user2 = getMessagesController().getUser(chatInfo.participants.participants.get(lhs).user_id); int status1 = 0; int status2 = 0; if (user1 != null) { @@ -3796,11 +4727,14 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. chatInfo = value; if (chatInfo != null && chatInfo.migrated_from_chat_id != 0 && mergeDialogId == 0) { mergeDialogId = -chatInfo.migrated_from_chat_id; - MediaDataController.getInstance(currentAccount).getMediaCounts(mergeDialogId, classGuid); + getMediaDataController().getMediaCounts(mergeDialogId, classGuid); } if (sharedMediaLayout != null) { sharedMediaLayout.setChatInfo(chatInfo); } + if (avatarsViewPager != null) { + avatarsViewPager.setChatInfo(chatInfo); + } fetchUsersFromChannelInfo(); } @@ -3826,15 +4760,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private void kickUser(int uid) { if (uid != 0) { - MessagesController.getInstance(currentAccount).deleteUserFromChat(chat_id, MessagesController.getInstance(currentAccount).getUser(uid), chatInfo); + getMessagesController().deleteUserFromChat(chat_id, getMessagesController().getUser(uid), chatInfo); } else { - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.closeChats); + getNotificationCenter().removeObserver(this, NotificationCenter.closeChats); if (AndroidUtilities.isTablet()) { - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats, -(long) chat_id); + getNotificationCenter().postNotificationName(NotificationCenter.closeChats, -(long) chat_id); } else { - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); + getNotificationCenter().postNotificationName(NotificationCenter.closeChats); } - MessagesController.getInstance(currentAccount).deleteUserFromChat(chat_id, MessagesController.getInstance(currentAccount).getUser(UserConfig.getInstance(currentAccount).getClientUserId()), chatInfo); + getMessagesController().deleteUserFromChat(chat_id, getMessagesController().getUser(getUserConfig().getClientUserId()), chatInfo); playProfileAnimation = 0; finishFragment(); } @@ -3848,6 +4782,34 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. int prevRowsCount = rowCount; rowCount = 0; + setAvatarRow = -1; + setAvatarSectionRow = -1; + numberSectionRow = -1; + numberRow = -1; + setUsernameRow = -1; + bioRow = -1; + settingsSectionRow = -1; + settingsSectionRow2 = -1; + notificationRow = -1; + nekoRow = -1; + languageRow = -1; + privacyRow = -1; + dataRow = -1; + chatRow = -1; + filtersRow = -1; + devicesRow = -1; + devicesSectionRow = -1; + helpHeaderRow = -1; + questionRow = -1; + faqRow = -1; + policyRow = -1; + helpSectionCell = -1; + debugHeaderRow = -1; + sendLogsRow = -1; + clearLogsRow = -1; + switchBackendRow = -1; + versionRow = -1; + sendMessageRow = -1; reportRow = -1; emptyRow = -1; @@ -3862,7 +4824,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. notificationsDividerRow = -1; notificationsRow = -1; infoSectionRow = -1; - settingsSectionRow = -1; + secretSettingsSectionRow = -1; bottomPaddingRow = -1; membersHeaderRow = -1; @@ -3890,54 +4852,96 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } - if (user_id != 0 && LocaleController.isRTL) { - emptyRow = rowCount++; - } - if (user_id != 0) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); - - boolean hasInfo = userInfo != null && !TextUtils.isEmpty(userInfo.about) || user != null && !TextUtils.isEmpty(user.username); - boolean hasPhone = user != null && !TextUtils.isEmpty(user.phone); - - infoHeaderRow = rowCount++; - if (!isBot && (hasPhone || !hasPhone && !hasInfo)) { - phoneRow = rowCount++; + if (LocaleController.isRTL) { + emptyRow = rowCount++; } - if (userInfo != null && !TextUtils.isEmpty(userInfo.about)) { - userInfoRow = rowCount++; - } - if (user != null && !TextUtils.isEmpty(user.username)) { + TLRPC.User user = getMessagesController().getUser(user_id); + + if (UserObject.isUserSelf(user)) { + if (avatarBig == null && (user.photo == null || !(user.photo.photo_big instanceof TLRPC.TL_fileLocation_layer97) && !(user.photo.photo_big instanceof TLRPC.TL_fileLocationToBeDeprecated)) && (avatarsViewPager == null || avatarsViewPager.getRealCount() == 0)) { + setAvatarRow = rowCount++; + setAvatarSectionRow = rowCount++; + } + numberSectionRow = rowCount++; + numberRow = rowCount++; usernameRow = rowCount++; - } - if (phoneRow != -1 || userInfoRow != -1 || usernameRow != -1) { - notificationsDividerRow = rowCount++; - } - if (user_id != UserConfig.getInstance(currentAccount).getClientUserId()) { - notificationsRow = rowCount++; - } - infoSectionRow = rowCount++; - - if (currentEncryptedChat instanceof TLRPC.TL_encryptedChat) { - settingsTimerRow = rowCount++; - settingsKeyRow = rowCount++; + bioRow = rowCount++; settingsSectionRow = rowCount++; - } + settingsSectionRow2 = rowCount++; + notificationRow = rowCount++; + dataRow = rowCount++; + privacyRow = rowCount++; + chatRow = rowCount++; + stickersRow = rowCount++; + filtersRow = rowCount++; + devicesRow = -1; + nekoRow = rowCount++; + languageRow = rowCount++; + devicesSectionRow = -1; + helpHeaderRow = rowCount++; + questionRow = -1; + faqRow = rowCount++; + policyRow = -1; + if (BuildVars.LOGS_ENABLED) { + helpSectionCell = rowCount++; + debugHeaderRow = rowCount++; + } else { + helpSectionCell = -1; + debugHeaderRow = -1; + } + if (BuildVars.LOGS_ENABLED) { + sendLogsRow = rowCount++; + clearLogsRow = rowCount++; + } else { + sendLogsRow = -1; + clearLogsRow = -1; + } + switchBackendRow = -1; + versionRow = rowCount++; + } else { + boolean hasInfo = userInfo != null && !TextUtils.isEmpty(userInfo.about) || user != null && !TextUtils.isEmpty(user.username); + boolean hasPhone = user != null && !TextUtils.isEmpty(user.phone); - if (user != null && !isBot && currentEncryptedChat == null && user.id != UserConfig.getInstance(currentAccount).getClientUserId()) { - if (userBlocked) { - unblockRow = rowCount++; + infoHeaderRow = rowCount++; + if (!isBot && (hasPhone || !hasPhone && !hasInfo)) { + phoneRow = rowCount++; + } + if (userInfo != null && !TextUtils.isEmpty(userInfo.about)) { + userInfoRow = rowCount++; + } + if (user != null && !TextUtils.isEmpty(user.username)) { + usernameRow = rowCount++; + } + if (phoneRow != -1 || userInfoRow != -1 || usernameRow != -1) { + notificationsDividerRow = rowCount++; + } + if (user_id != getUserConfig().getClientUserId()) { + notificationsRow = rowCount++; + } + infoSectionRow = rowCount++; + + if (currentEncryptedChat instanceof TLRPC.TL_encryptedChat) { + settingsTimerRow = rowCount++; + settingsKeyRow = rowCount++; + secretSettingsSectionRow = rowCount++; + } + + if (user != null && !isBot && currentEncryptedChat == null && user.id != getUserConfig().getClientUserId()) { + if (userBlocked) { + unblockRow = rowCount++; + lastSectionRow = rowCount++; + } + } + + if (hasMedia || userInfo != null && userInfo.common_chats_count != 0) { + sharedMediaRow = rowCount++; + } else if (lastSectionRow == -1 && needSendMessage) { + sendMessageRow = rowCount++; + reportRow = rowCount++; lastSectionRow = rowCount++; } } - - if (hasMedia || userInfo != null && userInfo.common_chats_count != 0) { - sharedMediaRow = rowCount++; - } else if (lastSectionRow == -1 && needSendMessage) { - sendMessageRow = rowCount++; - reportRow = rowCount++; - lastSectionRow = rowCount++; - } } else if (chat_id != 0) { if (chatInfo != null && (!TextUtils.isEmpty(chatInfo.about) || chatInfo.location instanceof TLRPC.TL_channelLocation) || !TextUtils.isEmpty(currentChat.username)) { infoHeaderRow = rowCount++; @@ -3973,7 +4977,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (ChatObject.isChannel(currentChat)) { if (chatInfo != null && currentChat.megagroup && chatInfo.participants != null && !chatInfo.participants.participants.isEmpty()) { - if (!ChatObject.isNotInChat(currentChat) && currentChat.megagroup && ChatObject.canAddUsers(currentChat) && (chatInfo == null || chatInfo.participants_count < MessagesController.getInstance(currentAccount).maxMegagroupCount)) { + if (!ChatObject.isNotInChat(currentChat) && currentChat.megagroup && ChatObject.canAddUsers(currentChat) && (chatInfo == null || chatInfo.participants_count < getMessagesController().maxMegagroupCount)) { addMemberRow = rowCount++; } int count = chatInfo.participants.participants.size(); @@ -4068,11 +5072,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } private void updateProfileData() { - if (avatarImage == null || nameTextView == null) { + if (avatarContainer == null || nameTextView == null) { return; } String onlineTextOverride; - int currentConnectionState = ConnectionsManager.getInstance(currentAccount).getConnectionState(); + int currentConnectionState = getConnectionsManager().getConnectionState(); if (currentConnectionState == ConnectionsManager.ConnectionStateWaitingForNetwork) { onlineTextOverride = LocaleController.getString("WaitingForNetwork", R.string.WaitingForNetwork); } else if (currentConnectionState == ConnectionsManager.ConnectionStateConnecting) { @@ -4087,23 +5091,40 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. int id = 0; if (user_id != 0) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + TLRPC.User user = getMessagesController().getUser(user_id); TLRPC.FileLocation photoBig = null; if (user.photo != null) { photoBig = user.photo.photo_big; } avatarDrawable.setInfo(user); + final ImageLocation imageLocation = ImageLocation.getForUser(user, true); final ImageLocation thumbLocation = ImageLocation.getForUser(user, false); + final ImageLocation videoLocation = avatarsViewPager.getCurrentVideoLocation(thumbLocation, imageLocation); avatarsViewPager.initIfEmpty(imageLocation, thumbLocation); - avatarImage.setImage(thumbLocation, "50_50", avatarDrawable, user); - FileLoader.getInstance(currentAccount).loadFile(imageLocation, user, null, 0, 1); + String filter; + if (videoLocation != null && videoLocation.imageType == FileLoader.IMAGE_TYPE_ANIMATION) { + filter = ImageLoader.AUTOPLAY_FILTER; + } else { + filter = null; + } + if (avatarBig == null) { + avatarImage.setImage(videoLocation, filter, thumbLocation, "50_50", avatarDrawable, user); + } + if (thumbLocation != null && setAvatarRow != -1 || thumbLocation == null && setAvatarRow == -1) { + int prevValue = setAvatarRow; + updateRowsIds(); + if (prevValue != setAvatarRow) { + listAdapter.notifyDataSetChanged(); + } + needLayout(true); + } + getFileLoader().loadFile(imageLocation, user, null, 0, 1); String newString = UserObject.getUserName(user); String newString2; - if (user.id == UserConfig.getInstance(currentAccount).getClientUserId()) { - newString2 = LocaleController.getString("ChatYourSelf", R.string.ChatYourSelf); - newString = LocaleController.getString("ChatYourSelfName", R.string.ChatYourSelfName); + if (user.id == getUserConfig().getClientUserId()) { + newString2 = LocaleController.getString("Online", R.string.Online); } else if (user.id == 333000 || user.id == 777000 || user.id == 42777) { newString2 = LocaleController.getString("ServiceNotifications", R.string.ServiceNotifications); } else if (MessagesController.isSupportUser(user)) { @@ -4125,7 +5146,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (nameTextView[a] == null) { continue; } - if (!nameTextView[a].getText().equals(newString)) { + if (a == 0 && user.id != getUserConfig().getClientUserId() && user.id / 1000 != 777 && user.id / 1000 != 333 && user.phone != null && user.phone.length() != 0 && getContactsController().contactsDict.get(user.id) == null && + (getContactsController().contactsDict.size() != 0 || !getContactsController().isLoadingContacts())) { + String phoneString = PhoneFormat.getInstance().format("+" + user.phone); + nameTextView[a].setText(phoneString); + } else { nameTextView[a].setText(newString); } if (a == 0 && onlineTextOverride != null) { @@ -4139,7 +5164,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (user.scam) { rightIcon = getScamDrawable(); } else { - rightIcon = MessagesController.getInstance(currentAccount).isDialogMuted(dialog_id != 0 ? dialog_id : (long) user_id) ? Theme.chat_muteIconDrawable : null; + rightIcon = getMessagesController().isDialogMuted(dialog_id != 0 ? dialog_id : (long) user_id) ? Theme.chat_muteIconDrawable : null; } } else if (user.scam) { rightIcon = getScamDrawable(); @@ -4158,7 +5183,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. id = user_id; avatarImage.getImageReceiver().setVisible(!PhotoViewer.isShowingImage(photoBig), false); } else if (chat_id != 0) { - TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(chat_id); + TLRPC.Chat chat = getMessagesController().getChat(chat_id); if (chat != null) { currentChat = chat; } else { @@ -4250,7 +5275,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (chat.scam) { nameTextView[a].setRightDrawable(getScamDrawable()); } else { - nameTextView[a].setRightDrawable(MessagesController.getInstance(currentAccount).isDialogMuted(-chat_id) ? Theme.chat_muteIconDrawable : null); + nameTextView[a].setRightDrawable(getMessagesController().isDialogMuted(-chat_id) ? Theme.chat_muteIconDrawable : null); } } if (a == 0 && onlineTextOverride != null) { @@ -4283,7 +5308,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } id = chat_id; if (changed) { - needLayout(); + needLayout(true); } TLRPC.FileLocation photoBig = null; @@ -4293,9 +5318,24 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. avatarDrawable.setInfo(chat); final ImageLocation imageLocation = ImageLocation.getForChat(chat, true); final ImageLocation thumbLocation = ImageLocation.getForChat(chat, false); - avatarsViewPager.initIfEmpty(imageLocation, thumbLocation); - avatarImage.setImage(thumbLocation, "50_50", avatarDrawable, chat); - FileLoader.getInstance(currentAccount).loadFile(imageLocation, chat, null, 0, 1); + final ImageLocation videoLocation = avatarsViewPager.getCurrentVideoLocation(thumbLocation, imageLocation); + boolean initied = avatarsViewPager.initIfEmpty(imageLocation, thumbLocation); + if ((imageLocation == null || initied) && isPulledDown) { + final View view = layoutManager.findViewByPosition(0); + if (view != null) { + listView.smoothScrollBy(0, view.getTop() - AndroidUtilities.dp(88), CubicBezierInterpolator.EASE_OUT_QUINT); + } + } + String filter; + if (videoLocation != null && videoLocation.imageType == FileLoader.IMAGE_TYPE_ANIMATION) { + filter = ImageLoader.AUTOPLAY_FILTER; + } else { + filter = null; + } + if (avatarBig == null) { + avatarImage.setImage(videoLocation, filter, thumbLocation, "50_50", avatarDrawable, chat); + } + getFileLoader().loadFile(imageLocation, chat, null, 0, 1); avatarImage.getImageReceiver().setVisible(!PhotoViewer.isShowingImage(photoBig), false); if (chat.photo != null && chat.photo.dc_id != 0) { idTextView.setText("ID: " + chat_id + ", DC: " + chat.photo.dc_id); @@ -4336,15 +5376,18 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. canSearchMembers = false; if (user_id != 0) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); - if (UserConfig.getInstance(currentAccount).getClientUserId() != user_id) { - if (user == null) { - return; - } + TLRPC.User user = getMessagesController().getUser(user_id); + if (user == null) { + return; + } + if (UserObject.isUserSelf(user)) { + otherItem.addSubItem(edit_name, R.drawable.msg_edit, LocaleController.getString("EditName", R.string.EditName)); + otherItem.addSubItem(logout, R.drawable.msg_leave, LocaleController.getString("LogOut", R.string.LogOut)); + } else { if (userInfo != null && userInfo.phone_calls_available) { callItemVisible = true; } - if (isBot || ContactsController.getInstance(currentAccount).contactsDict.get(user_id) == null) { + if (isBot || getContactsController().contactsDict.get(user_id) == null) { if (MessagesController.isSupportUser(user)) { if (userBlocked) { otherItem.addSubItem(block_contact, R.drawable.baseline_block_24, LocaleController.getString("Unblock", R.string.Unblock)); @@ -4376,20 +5419,19 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. otherItem.addSubItem(edit_contact, R.drawable.baseline_edit_24, LocaleController.getString("EditContact", R.string.EditContact)); otherItem.addSubItem(delete_contact, R.drawable.baseline_delete_24, LocaleController.getString("DeleteContact", R.string.DeleteContact)); } - } else { - otherItem.addSubItem(share_contact, R.drawable.baseline_forward_24, LocaleController.getString("ShareContact", R.string.ShareContact)); - } - if (!UserObject.isDeleted(user) && !isBot && currentEncryptedChat == null && user_id != getUserConfig().getClientUserId() && !userBlocked && user_id != 333000 && user_id != 777000 && user_id != 42777) { - otherItem.addSubItem(start_secret_chat, R.drawable.deproko_baseline_lock_24, LocaleController.getString("StartEncryptedChat", R.string.StartEncryptedChat)); + if (!UserObject.isDeleted(user) && !isBot && currentEncryptedChat == null && !userBlocked && user_id != 333000 && user_id != 777000 && user_id != 42777) { + otherItem.addSubItem(start_secret_chat, R.drawable.msg_start_secret, LocaleController.getString("StartEncryptedChat", R.string.StartEncryptedChat)); + } + otherItem.addSubItem(add_shortcut, R.drawable.msg_home, LocaleController.getString("AddShortcut", R.string.AddShortcut)); } } else if (chat_id != 0) { if (chat_id > 0) { - TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(chat_id); + TLRPC.Chat chat = getMessagesController().getChat(chat_id); if (ChatObject.isChannel(chat)) { if (ChatObject.hasAdminRights(chat) || chat.megagroup) { editItemVisible = true; } - if (!chat.megagroup && chatInfo != null && chatInfo.can_view_stats) { + if (chatInfo != null && chatInfo.can_view_stats) { otherItem.addSubItem(statistics, R.drawable.msg_stats, LocaleController.getString("Statistics", R.string.Statistics)); } if (!TextUtils.isEmpty(chat.username)) { @@ -4419,13 +5461,25 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. otherItem.addSubItem(leave_group, R.drawable.baseline_exit_to_app_24, LocaleController.getString("DeleteAndExit", R.string.DeleteAndExit)); } } + otherItem.addSubItem(add_shortcut, R.drawable.baseline_home_24, LocaleController.getString("AddShortcut", R.string.AddShortcut)); + } + + if (imageUpdater != null) { + otherItem.addSubItem(add_photo, R.drawable.msg_addphoto, LocaleController.getString("AddPhoto", R.string.AddPhoto)); + otherItem.addSubItem(set_as_main, R.drawable.menu_private, LocaleController.getString("SetAsMain", R.string.SetAsMain)); + otherItem.addSubItem(gallery_menu_save, R.drawable.baseline_image_24, LocaleController.getString("SaveToGallery", R.string.SaveToGallery)); + //otherItem.addSubItem(edit_avatar, R.drawable.photo_paint, LocaleController.getString("EditPhoto", R.string.EditPhoto)); + otherItem.addSubItem(delete_avatar, R.drawable.msg_delete, LocaleController.getString("Delete", R.string.Delete)); + } else { + otherItem.addSubItem(gallery_menu_save, R.drawable.msg_gallery, LocaleController.getString("SaveToGallery", R.string.SaveToGallery)); } - otherItem.addSubItem(add_shortcut, R.drawable.baseline_home_24, LocaleController.getString("AddShortcut", R.string.AddShortcut)); - otherItem.addSubItem(gallery_menu_save, R.drawable.baseline_image_24, LocaleController.getString("SaveToGallery", R.string.SaveToGallery)); if (!isPulledDown) { otherItem.hideSubItem(gallery_menu_save); + otherItem.hideSubItem(set_as_main); + otherItem.hideSubItem(add_photo); + otherItem.hideSubItem(edit_avatar); + otherItem.hideSubItem(delete_avatar); } - otherItem.setContentDescription(LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions)); if (callItemVisible) { if (callItem.getVisibility() != View.VISIBLE) { @@ -4436,7 +5490,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. callItem.setVisibility(View.GONE); } } - if (editItemVisible) { if (editItem.getVisibility() != View.VISIBLE) { editItem.setVisibility(View.VISIBLE); @@ -4446,13 +5499,6 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. editItem.setVisibility(View.GONE); } } - - if (editItem != null) { - editItem.setContentDescription(LocaleController.getString("Edit", R.string.Edit)); - } - if (callItem != null) { - callItem.setContentDescription(LocaleController.getString("Call", R.string.Call)); - } if (avatarsViewPagerIndicatorView != null) { if (avatarsViewPagerIndicatorView.isIndicatorFullyVisible()) { if (editItemVisible) { @@ -4487,22 +5533,25 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else { args.putInt("enc_id", (int) (did >> 32)); } - if (!MessagesController.getInstance(currentAccount).checkCanOpenChat(args, fragment)) { + if (!getMessagesController().checkCanOpenChat(args, fragment)) { return; } - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.closeChats); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.closeChats); + getNotificationCenter().removeObserver(this, NotificationCenter.closeChats); + getNotificationCenter().postNotificationName(NotificationCenter.closeChats); presentFragment(new ChatActivity(args), true); removeSelfFromStack(); - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); - SendMessagesHelper.getInstance(currentAccount).sendMessage(user, did, null, null, null, true, 0); + TLRPC.User user = getMessagesController().getUser(user_id); + getSendMessagesHelper().sendMessage(user, did, null, null, null, true, 0); } @Override public void onRequestPermissionsResultFragment(int requestCode, String[] permissions, int[] grantResults) { + if (imageUpdater != null) { + imageUpdater.onRequestPermissionsResultFragment(requestCode, permissions, grantResults); + } if (requestCode == 101) { - final TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + final TLRPC.User user = getMessagesController().getUser(user_id); if (user == null) { return; } @@ -4514,6 +5563,358 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } + @Override + public void dismissCurrentDialog() { + if (imageUpdater != null && imageUpdater.dismissCurrentDialog(visibleDialog)) { + return; + } + super.dismissCurrentDialog(); + } + + @Override + public boolean dismissDialogOnPause(Dialog dialog) { + return (imageUpdater == null || imageUpdater.dismissDialogOnPause(dialog)) && super.dismissDialogOnPause(dialog); + } + + private Animator searchExpandTransition(boolean enter) { + if (enter) { + getParentActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); + } + if (searchViewTransition != null) { + searchViewTransition.removeAllListeners(); + searchViewTransition.cancel(); + } + ValueAnimator valueAnimator = ValueAnimator.ofFloat(searchTransitionProgress, enter ? 0f : 1f); + float offset = extraHeight; + searchListView.setTranslationY(offset); + searchListView.setVisibility(View.VISIBLE); + searchItem.setVisibility(View.VISIBLE); + + listView.setVisibility(View.VISIBLE); + + needLayout(true); + + avatarContainer.setVisibility(View.VISIBLE); + nameTextView[1].setVisibility(View.VISIBLE); + onlineTextView[1].setVisibility(View.VISIBLE); + idTextView.setVisibility(View.VISIBLE); + + actionBar.onSearchFieldVisibilityChanged(searchTransitionProgress > 0.5f); + if (otherItem != null) { + otherItem.setVisibility(searchTransitionProgress > 0.5f ? View.VISIBLE : View.GONE); + } + searchItem.setVisibility(searchTransitionProgress > 0.5f ? View.VISIBLE : View.GONE); + + searchItem.getSearchContainer().setVisibility(searchTransitionProgress > 0.5f ? View.GONE : View.VISIBLE); + searchListView.setEmptyView(emptyView); + avatarContainer.setClickable(false); + + valueAnimator.addUpdateListener(animation -> { + searchTransitionProgress = (float) valueAnimator.getAnimatedValue(); + float progressHalf = (searchTransitionProgress - 0.5f) / 0.5f; + float progressHalfEnd = (0.5f - searchTransitionProgress) / 0.5f; + if (progressHalf < 0) { + progressHalf = 0f; + } + if (progressHalfEnd < 0) { + progressHalfEnd = 0f; + } + + searchTransitionOffset = (int) (-offset * (1f - searchTransitionProgress)); + searchListView.setTranslationY(offset * searchTransitionProgress); + emptyView.setTranslationY(offset * searchTransitionProgress); + listView.setTranslationY(-offset * (1f - searchTransitionProgress)); + needLayout(true); + + listView.setAlpha(progressHalf); + searchListView.setAlpha(1f - progressHalf); + emptyView.setAlpha(1f - progressHalf); + + avatarContainer.setAlpha(progressHalf); + nameTextView[1].setAlpha(progressHalf); + onlineTextView[1].setAlpha(progressHalf); + + searchItem.getSearchField().setAlpha(progressHalfEnd); + if (enter && searchTransitionProgress < 0.7f) { + searchItem.requestFocusOnSearchView(); + } + + searchItem.getSearchContainer().setVisibility(searchTransitionProgress < 0.5f ? View.VISIBLE : View.GONE); + if (otherItem != null) { + otherItem.setVisibility(searchTransitionProgress > 0.5f ? View.VISIBLE : View.GONE); + } + searchItem.setVisibility(searchTransitionProgress > 0.5f ? View.VISIBLE : View.GONE); + + actionBar.onSearchFieldVisibilityChanged(searchTransitionProgress < 0.5f); + + if (otherItem != null) { + otherItem.setAlpha(progressHalf); + } + searchItem.setAlpha(progressHalf); + topView.invalidate(); + }); + + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + updateSearchViewState(enter); + avatarContainer.setClickable(true); + if (enter) { + searchItem.requestFocusOnSearchView(); + } + needLayout(true); + } + }); + + valueAnimator.setDuration(180); + valueAnimator.setInterpolator(AndroidUtilities.decelerateInterpolator); + searchViewTransition = valueAnimator; + return valueAnimator; + } + + private void updateSearchViewState(boolean enter) { + int hide = enter ? View.GONE : View.VISIBLE; + listView.setVisibility(hide); + searchListView.setVisibility(enter ? View.VISIBLE : View.GONE); + searchItem.getSearchContainer().setVisibility(enter ? View.VISIBLE : View.GONE); + + actionBar.onSearchFieldVisibilityChanged(enter); + + avatarContainer.setVisibility(hide); + nameTextView[1].setVisibility(hide); + onlineTextView[1].setVisibility(hide); + idTextView.setVisibility(hide); + + if (otherItem != null) { + otherItem.setAlpha(1f); + otherItem.setVisibility(hide); + } + searchItem.setVisibility(hide); + + avatarContainer.setAlpha(1f); + nameTextView[1].setAlpha(1f); + onlineTextView[1].setAlpha(1f); + searchItem.setAlpha(1f); + listView.setAlpha(1f); + searchListView.setAlpha(1f); + emptyView.setAlpha(1f); + if (enter) { + searchListView.setEmptyView(emptyView); + } else { + emptyView.setVisibility(View.GONE); + } + } + + @Override + public void onUploadProgressChanged(float progress) { + if (avatarProgressView == null) { + return; + } + avatarProgressView.setProgress(progress); + } + + @Override + public void didStartUpload(boolean isVideo) { + if (avatarProgressView == null) { + return; + } + avatarProgressView.setProgress(0.0f); + } + + @Override + public void didUploadPhoto(final TLRPC.InputFile photo, final TLRPC.InputFile video, double videoStartTimestamp, String videoPath, TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { + AndroidUtilities.runOnUIThread(() -> { + if (photo != null || video != null) { + TLRPC.TL_photos_uploadProfilePhoto req = new TLRPC.TL_photos_uploadProfilePhoto(); + if (photo != null) { + req.file = photo; + req.flags |= 1; + } + if (video != null) { + req.video = video; + req.flags |= 2; + req.video_start_ts = videoStartTimestamp; + req.flags |= 4; + } + getConnectionsManager().sendRequest(req, (response, error) -> { + if (error == null) { + TLRPC.User user = getMessagesController().getUser(getUserConfig().getClientUserId()); + if (user == null) { + user = getUserConfig().getCurrentUser(); + if (user == null) { + return; + } + getMessagesController().putUser(user, false); + } else { + getUserConfig().setCurrentUser(user); + } + TLRPC.TL_photos_photo photos_photo = (TLRPC.TL_photos_photo) response; + ArrayList sizes = photos_photo.photo.sizes; + TLRPC.PhotoSize small = FileLoader.getClosestPhotoSizeWithSize(sizes, 150); + TLRPC.PhotoSize big = FileLoader.getClosestPhotoSizeWithSize(sizes, 800); + TLRPC.VideoSize videoSize = photos_photo.photo.video_sizes.isEmpty() ? null : photos_photo.photo.video_sizes.get(0); + user.photo = new TLRPC.TL_userProfilePhoto(); + user.photo.photo_id = photos_photo.photo.id; + if (small != null) { + user.photo.photo_small = small.location; + } + if (big != null) { + user.photo.photo_big = big.location; + } else if (small != null) { + user.photo.photo_small = small.location; + } + + if (photo != null || video != null) { + if (small != null && avatar != null) { + File destFile = FileLoader.getPathToAttach(small, true); + File src = FileLoader.getPathToAttach(avatar, true); + src.renameTo(destFile); + String oldKey = avatar.volume_id + "_" + avatar.local_id + "@50_50"; + String newKey = small.location.volume_id + "_" + small.location.local_id + "@50_50"; + ImageLoader.getInstance().replaceImageInCache(oldKey, newKey, ImageLocation.getForUser(user, false), true); + } + if (big != null && avatarBig != null) { + File destFile = FileLoader.getPathToAttach(big, true); + File src = FileLoader.getPathToAttach(avatarBig, true); + src.renameTo(destFile); + } + if (videoSize != null && videoPath != null) { + File destFile = FileLoader.getPathToAttach(videoSize, "mp4", true); + File src = new File(videoPath); + src.renameTo(destFile); + } + } + + getMessagesStorage().clearUserPhotos(user.id); + ArrayList users = new ArrayList<>(); + users.add(user); + getMessagesStorage().putUsersAndChats(users, null, false, true); + } + AndroidUtilities.runOnUIThread(() -> { + allowPullingDown = !AndroidUtilities.isTablet() && !isInLandscapeMode && avatarImage.getImageReceiver().hasNotThumb(); + avatar = null; + avatarBig = null; + updateProfileData(); + showAvatarProgress(false, true); + getNotificationCenter().postNotificationName(NotificationCenter.updateInterfaces, MessagesController.UPDATE_MASK_ALL); + getNotificationCenter().postNotificationName(NotificationCenter.mainUserInfoChanged); + getUserConfig().saveConfig(true); + }); + }); + } else { + allowPullingDown = false; + avatar = smallSize.location; + avatarBig = bigSize.location; + avatarImage.setImage(ImageLocation.getForLocal(avatar), "50_50", avatarDrawable, null); + if (setAvatarRow != -1) { + updateRowsIds(); + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + needLayout(true); + } + showAvatarProgress(true, false); + final View view = layoutManager.findViewByPosition(0); + if (view != null && isPulledDown) { + listView.smoothScrollBy(0, view.getTop() - AndroidUtilities.dp(88), CubicBezierInterpolator.EASE_OUT_QUINT); + } + } + }); + } + + private void showAvatarProgress(boolean show, boolean animated) { + if (avatarProgressView == null) { + return; + } + if (avatarAnimation != null) { + avatarAnimation.cancel(); + avatarAnimation = null; + } + if (animated) { + avatarAnimation = new AnimatorSet(); + if (show) { + avatarProgressView.setVisibility(View.VISIBLE); + avatarAnimation.playTogether(ObjectAnimator.ofFloat(avatarProgressView, View.ALPHA, 1.0f)); + } else { + avatarAnimation.playTogether(ObjectAnimator.ofFloat(avatarProgressView, View.ALPHA, 0.0f)); + } + avatarAnimation.setDuration(180); + avatarAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (avatarAnimation == null || avatarProgressView == null) { + return; + } + if (!show) { + avatarProgressView.setVisibility(View.INVISIBLE); + } + avatarAnimation = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + avatarAnimation = null; + } + }); + avatarAnimation.start(); + } else { + if (show) { + avatarProgressView.setAlpha(1.0f); + avatarProgressView.setVisibility(View.VISIBLE); + } else { + avatarProgressView.setAlpha(0.0f); + avatarProgressView.setVisibility(View.INVISIBLE); + } + } + } + + @Override + public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { + if (imageUpdater != null) { + imageUpdater.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void saveSelfArgs(Bundle args) { + if (imageUpdater != null && imageUpdater.currentPicturePath != null) { + args.putString("path", imageUpdater.currentPicturePath); + } + } + + @Override + public void restoreSelfArgs(Bundle args) { + if (imageUpdater != null) { + imageUpdater.currentPicturePath = args.getString("path"); + } + } + + private void sendLogs() { + + File path = new File(EnvUtil.getTelegramPath(), "logs"); + + File logcatFile = new File(path, "NekoX-" + System.currentTimeMillis() + ".log"); + + FileUtil.delete(logcatFile); + + try { + + Process process = RuntimeUtil.exec("logcat", "-d"); + + IoUtil.copy(process, logcatFile); + + RuntimeUtil.exec("logcat", "-c"); + + ShareUtil.shareFile(getParentActivity(), logcatFile); + + } catch (Exception e) { + + AlertUtil.showToast(e); + + } + + } + private class ListAdapter extends RecyclerListView.SelectionAdapter { private Context mContext; @@ -4531,7 +5932,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. break; } case 2: { - view = new TextDetailCell(mContext); + final TextDetailCell textDetailCell = new TextDetailCell(mContext); + textDetailCell.setContentDescriptionValueFirst(true); + view = textDetailCell; break; } case 3: { @@ -4539,7 +5942,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override protected void didPressUrl(String url) { if (url.startsWith("@")) { - MessagesController.getInstance(currentAccount).openByUserName(url.substring(1), ProfileActivity.this, 0); + getMessagesController().openByUserName(url.substring(1), ProfileActivity.this, 0); } else if (url.startsWith("#")) { DialogsActivity fragment = new DialogsActivity(null); fragment.setSearchString(url); @@ -4630,6 +6033,23 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. view = sharedMediaLayout; break; } + case 14: { + TextInfoPrivacyCell cell = new TextInfoPrivacyCell(mContext, 10); + cell.getTextView().setGravity(Gravity.CENTER_HORIZONTAL); + cell.getTextView().setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText3)); + cell.getTextView().setMovementMethod(null); + cell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); + + cell.setText("Nekogram X v" + BuildConfig.VERSION_NAME + " " + FileUtil.getAbi() + " " + BuildConfig.BUILD_TYPE); + + cell.getTextView().setPadding(0, AndroidUtilities.dp(14), 0, AndroidUtilities.dp(14)); + view = cell; + Drawable drawable = Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow); + CombinedDrawable combinedDrawable = new CombinedDrawable(new ColorDrawable(Theme.getColor(Theme.key_windowBackgroundGray)), drawable); + combinedDrawable.setFullsize(true); + view.setBackgroundDrawable(combinedDrawable); + break; + } } if (viewType != 13) { view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); @@ -4664,13 +6084,21 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } else if (position == membersHeaderRow) { headerCell.setText(LocaleController.getString("ChannelMembers", R.string.ChannelMembers)); + } else if (position == settingsSectionRow2) { + headerCell.setText(LocaleController.getString("SETTINGS", R.string.SETTINGS)); + } else if (position == numberSectionRow) { + headerCell.setText(LocaleController.getString("Account", R.string.Account)); + } else if (position == helpHeaderRow) { + headerCell.setText(LocaleController.getString("SettingsHelp", R.string.SettingsHelp)); + } else if (position == debugHeaderRow) { + headerCell.setText(LocaleController.getString("SettingsDebug", R.string.SettingsDebug)); } break; case 2: TextDetailCell detailCell = (TextDetailCell) holder.itemView; if (position == phoneRow) { String text; - final TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + final TLRPC.User user = getMessagesController().getUser(user_id); if (!TextUtils.isEmpty(user.phone) && !(NekoConfig.hidePhone && user.id == UserConfig.getInstance(currentAccount).getClientUserId())) { text = PhoneFormat.getInstance().format("+" + user.phone); @@ -4681,7 +6109,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else if (position == usernameRow) { String text; if (user_id != 0) { - final TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(user_id); + final TLRPC.User user = getMessagesController().getUser(user_id); if (user != null && !TextUtils.isEmpty(user.username)) { text = "@" + user.username; } else { @@ -4689,14 +6117,46 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } detailCell.setTextAndValue(text, LocaleController.getString("Username", R.string.Username), false); } else if (currentChat != null) { - TLRPC.Chat chat = MessagesController.getInstance(currentAccount).getChat(chat_id); - detailCell.setTextAndValue(MessagesController.getInstance(currentAccount).linkPrefix + "/" + chat.username, LocaleController.getString("InviteLink", R.string.InviteLink), false); + TLRPC.Chat chat = getMessagesController().getChat(chat_id); + detailCell.setTextAndValue(getMessagesController().linkPrefix + "/" + chat.username, LocaleController.getString("InviteLink", R.string.InviteLink), false); } } else if (position == locationRow) { if (chatInfo != null && chatInfo.location instanceof TLRPC.TL_channelLocation) { TLRPC.TL_channelLocation location = (TLRPC.TL_channelLocation) chatInfo.location; detailCell.setTextAndValue(location.address, LocaleController.getString("AttachLocation", R.string.AttachLocation), false); } + } else if (position == numberRow) { + TLRPC.User user = UserConfig.getInstance(currentAccount).getCurrentUser(); + String value; + if (user != null && user.phone != null && user.phone.length() != 0) { + value = PhoneFormat.getInstance().format("+" + user.phone); + } else { + value = LocaleController.getString("NumberUnknown", R.string.NumberUnknown); + } + detailCell.setTextAndValue(value, LocaleController.getString("TapToChangePhone", R.string.TapToChangePhone), true); + detailCell.setContentDescriptionValueFirst(false); + } else if (position == setUsernameRow) { + TLRPC.User user = UserConfig.getInstance(currentAccount).getCurrentUser(); + String value; + if (user != null && !TextUtils.isEmpty(user.username)) { + value = "@" + user.username; + } else { + value = LocaleController.getString("UsernameEmpty", R.string.UsernameEmpty); + } + detailCell.setTextAndValue(value, LocaleController.getString("Username", R.string.Username), true); + detailCell.setContentDescriptionValueFirst(true); + } else if (position == bioRow) { + String value; + if (userInfo == null || !TextUtils.isEmpty(userInfo.about)) { + value = userInfo == null ? LocaleController.getString("Loading", R.string.Loading) : userInfo.about; + detailCell.setTextWithEmojiAndValue(value, LocaleController.getString("UserBio", R.string.UserBio), false); + detailCell.setContentDescriptionValueFirst(true); + currentBio = userInfo != null ? userInfo.about : null; + } else { + detailCell.setTextAndValue(LocaleController.getString("UserBio", R.string.UserBio), LocaleController.getString("UserBioDetail", R.string.UserBioDetail), false); + detailCell.setContentDescriptionValueFirst(false); + currentBio = null; + } } break; case 3: @@ -4716,7 +6176,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. textCell.setColors(Theme.key_windowBackgroundWhiteGrayIcon, Theme.key_windowBackgroundWhiteBlackText); textCell.setTag(Theme.key_windowBackgroundWhiteBlackText); if (position == settingsTimerRow) { - TLRPC.EncryptedChat encryptedChat = MessagesController.getInstance(currentAccount).getEncryptedChat((int) (dialog_id >> 32)); + TLRPC.EncryptedChat encryptedChat = getMessagesController().getEncryptedChat((int) (dialog_id >> 32)); String value; if (encryptedChat.ttl == 0) { value = LocaleController.getString("ShortMessageLifetimeForever", R.string.ShortMessageLifetimeForever); @@ -4729,12 +6189,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. textCell.setColors(null, Theme.key_windowBackgroundWhiteRedText5); } else if (position == settingsKeyRow) { IdenticonDrawable identiconDrawable = new IdenticonDrawable(); - TLRPC.EncryptedChat encryptedChat = MessagesController.getInstance(currentAccount).getEncryptedChat((int) (dialog_id >> 32)); + TLRPC.EncryptedChat encryptedChat = getMessagesController().getEncryptedChat((int) (dialog_id >> 32)); identiconDrawable.setEncryptedChat(encryptedChat); textCell.setTextAndValueDrawable(LocaleController.getString("EncryptionKey", R.string.EncryptionKey), identiconDrawable, false); } else if (position == joinRow) { textCell.setColors(null, Theme.key_windowBackgroundWhiteBlueText2); - textCell.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText2)); if (currentChat.megagroup) { textCell.setText(LocaleController.getString("ProfileJoinGroup", R.string.ProfileJoinGroup), false); } else { @@ -4774,6 +6233,35 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else if (position == reportRow) { textCell.setText(LocaleController.getString("ReportUserLocation", R.string.ReportUserLocation), false); textCell.setColors(null, Theme.key_windowBackgroundWhiteRedText5); + } else if (position == languageRow) { + textCell.setTextAndIcon(LocaleController.getString("Language", R.string.Language), R.drawable.baseline_language_24, false); + } else if (position == notificationRow) { + textCell.setTextAndIcon(LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, true); + } else if (position == privacyRow) { + textCell.setTextAndIcon(LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_lock_24, true); + } else if (position == dataRow) { + textCell.setTextAndIcon(LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, true); + } else if (position == chatRow) { + textCell.setTextAndIcon(LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_palette_24, true); + } else if (position == stickersRow) { + textCell.setTextAndIcon(LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.deproko_baseline_stickers_24, true); + } else if (position == nekoRow) { + textCell.setTextAndIcon(LocaleController.getString("NekoSettings", R.string.NekoSettings), R.drawable.baseline_extension_24, true); + } else if (position == filtersRow) { + textCell.setTextAndIcon(LocaleController.getString("Filters", R.string.Filters), R.drawable.baseline_folder_24, true); + } else if (position == questionRow) { + textCell.setTextAndIcon(LocaleController.getString("NekoXUpdatesChannel", R.string.NekoXUpdatesChannel), R.drawable.baseline_bullhorn_24, true); + } else if (position == faqRow) { + textCell.setTextAndIcon(LocaleController.getString("NekoXFaq", R.string.NekoXFaq), R.drawable.baseline_help_24, true); + } else if (position == sendLogsRow) { + textCell.setTextAndIcon(LocaleController.getString("DebugSendLogs", R.string.DebugSendLogs), R.drawable.baseline_bug_report_24, true); + } else if (position == clearLogsRow) { + textCell.setTextAndIcon(LocaleController.getString("DebugClearLogs", R.string.DebugClearLogs), R.drawable.baseline_delete_sweep_24, switchBackendRow != -1); + } else if (position == switchBackendRow) { + textCell.setText("Switch Backend", false); + } else if (position == setAvatarRow) { + textCell.setColors(Theme.key_windowBackgroundWhiteBlueIcon, Theme.key_windowBackgroundWhiteBlueButton); + textCell.setTextAndIcon(LocaleController.getString("SetProfilePhoto", R.string.SetProfilePhoto), R.drawable.msg_addphoto, false); } break; case 6: @@ -4796,7 +6284,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. int delta = preferences.getInt("notifyuntil_" + did, 0); String val; if (value == 3 && delta != Integer.MAX_VALUE) { - delta -= ConnectionsManager.getInstance(currentAccount).getCurrentTime(); + delta -= getConnectionsManager().getCurrentTime(); if (delta <= 0) { if (custom) { val = LocaleController.getString("NotificationsCustom", R.string.NotificationsCustom); @@ -4818,7 +6306,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (hasOverride) { enabled = true; } else { - enabled = NotificationsController.getInstance(currentAccount).isGlobalNotificationsEnabled(did); + enabled = getNotificationsController().isGlobalNotificationsEnabled(did); } } else if (value == 1) { enabled = true; @@ -4843,14 +6331,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. View sectionCell = holder.itemView; sectionCell.setTag(position); Drawable drawable; - if (position == infoSectionRow && lastSectionRow == -1 && settingsSectionRow == -1 && sharedMediaRow == -1 && membersSectionRow == -1 || position == settingsSectionRow || position == lastSectionRow || position == membersSectionRow && lastSectionRow == -1 && sharedMediaRow == -1) { - drawable = Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow); + if (position == infoSectionRow && lastSectionRow == -1 && secretSettingsSectionRow == -1 && sharedMediaRow == -1 && membersSectionRow == -1 || position == secretSettingsSectionRow || position == lastSectionRow || position == membersSectionRow && lastSectionRow == -1 && sharedMediaRow == -1) { + sectionCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); } else { - drawable = Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow); + sectionCell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow)); } - CombinedDrawable combinedDrawable = new CombinedDrawable(new ColorDrawable(Theme.getColor(Theme.key_windowBackgroundGray)), drawable); - combinedDrawable.setFullsize(true); - sectionCell.setBackgroundDrawable(combinedDrawable); break; case 8: UserCell userCell = (UserCell) holder.itemView; @@ -4885,7 +6370,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } userCell.setAdminRole(role); - userCell.setData(MessagesController.getInstance(currentAccount).getUser(part.user_id), null, null, 0, position != membersEndRow - 1); + userCell.setData(getMessagesController().getUser(part.user_id), null, null, 0, position != membersEndRow - 1); } break; } @@ -4893,6 +6378,16 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { + if (notificationRow != -1) { + int position = holder.getAdapterPosition(); + return position == notificationRow || position == numberRow || position == privacyRow || + position == languageRow || position == setUsernameRow || position == bioRow || + position == versionRow || position == dataRow || position == chatRow || + position == questionRow || position == devicesRow || position == filtersRow || + position == faqRow || position == policyRow || position == sendLogsRow || + position == clearLogsRow || position == switchBackendRow || position == setAvatarRow || + position == nekoRow; + } int type = holder.getItemViewType(); return type != 1 && type != 5 && type != 7 && type != 9 && type != 10 && type != 11 && type != 12 && type != 13; } @@ -4903,37 +6398,621 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } @Override - public int getItemViewType(int i) { - if (i == infoHeaderRow || i == membersHeaderRow) { + public int getItemViewType(int position) { + if (position == infoHeaderRow || position == membersHeaderRow || position == settingsSectionRow2 || + position == numberSectionRow || position == helpHeaderRow || position == debugHeaderRow) { return 1; - } else if (i == phoneRow || i == usernameRow || i == locationRow) { + } else if (position == phoneRow || position == usernameRow || position == locationRow || + position == numberRow || position == setUsernameRow || position == bioRow) { return 2; - } else if (i == userInfoRow || i == channelInfoRow) { + } else if (position == userInfoRow || position == channelInfoRow) { return 3; - } else if (i == settingsTimerRow || i == settingsKeyRow || i == reportRow || - i == subscribersRow || i == administratorsRow || i == blockedUsersRow || - i == addMemberRow || i == joinRow || i == unblockRow || - i == sendMessageRow) { + } else if (position == settingsTimerRow || position == settingsKeyRow || position == reportRow || + position == subscribersRow || position == administratorsRow || position == blockedUsersRow || + position == addMemberRow || position == joinRow || position == unblockRow || + position == sendMessageRow || position == notificationRow || position == privacyRow || + position == languageRow || position == dataRow || position == chatRow || + position == questionRow || position == devicesRow || position == filtersRow || + position == faqRow || position == policyRow || position == sendLogsRow || + position == clearLogsRow || position == switchBackendRow || position == setAvatarRow || + position == nekoRow || position == stickersRow) { return 4; - } else if (i == notificationsDividerRow) { + } else if (position == notificationsDividerRow) { return 5; - } else if (i == notificationsRow) { + } else if (position == notificationsRow) { return 6; - } else if (i == infoSectionRow || i == lastSectionRow || i == membersSectionRow || i == settingsSectionRow) { + } else if (position == infoSectionRow || position == lastSectionRow || position == membersSectionRow || + position == secretSettingsSectionRow || position == settingsSectionRow || position == devicesSectionRow || + position == helpSectionCell || position == setAvatarSectionRow) { return 7; - } else if (i >= membersStartRow && i < membersEndRow) { + } else if (position >= membersStartRow && position < membersEndRow) { return 8; - } else if (i == emptyRow) { + } else if (position == emptyRow) { return 11; - } else if (i == bottomPaddingRow) { + } else if (position == bottomPaddingRow) { return 12; - } else if (i == sharedMediaRow) { + } else if (position == sharedMediaRow) { return 13; + } else if (position == versionRow) { + return 14; } return 0; } } + private class SearchAdapter extends RecyclerListView.SelectionAdapter { + + private class SearchResult { + + private String searchTitle; + private Runnable openRunnable; + private String rowName; + private String[] path; + private int iconResId; + private int guid; + private int num; + + public SearchResult(int g, String search, int icon, Runnable open) { + this(g, search, null, null, null, icon, open); + } + + public SearchResult(int g, String search, String pathArg1, int icon, Runnable open) { + this(g, search, null, pathArg1, null, icon, open); + } + + public SearchResult(int g, String search, String row, String pathArg1, int icon, Runnable open) { + this(g, search, row, pathArg1, null, icon, open); + } + + public SearchResult(int g, String search, String row, String pathArg1, String pathArg2, int icon, Runnable open) { + guid = g; + searchTitle = search; + rowName = row; + openRunnable = open; + iconResId = icon; + if (pathArg1 != null && pathArg2 != null) { + path = new String[]{pathArg1, pathArg2}; + } else if (pathArg1 != null) { + path = new String[]{pathArg1}; + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SearchResult)) { + return false; + } + SearchResult result = (SearchResult) obj; + return guid == result.guid; + } + + @Override + public String toString() { + SerializedData data = new SerializedData(); + data.writeInt32(num); + data.writeInt32(1); + data.writeInt32(guid); + return Utilities.bytesToHex(data.toByteArray()); + } + + private void open() { + openRunnable.run(); + AndroidUtilities.scrollToFragmentRow(parentLayout, rowName); + } + } + + private SearchResult[] searchArray = new SearchResult[]{ + new SearchResult(500, LocaleController.getString("EditName", R.string.EditName), 0, () -> presentFragment(new ChangeNameActivity())), + new SearchResult(501, LocaleController.getString("ChangePhoneNumber", R.string.ChangePhoneNumber), 0, () -> presentFragment(new ActionIntroActivity(ActionIntroActivity.ACTION_TYPE_CHANGE_PHONE_NUMBER))), + new SearchResult(502, LocaleController.getString("AddAnotherAccount", R.string.AddAnotherAccount), 0, () -> { + int freeAccount = -1; + for (int a = 0; a < UserConfig.MAX_ACCOUNT_COUNT; a++) { + if (!UserConfig.getInstance(a).isClientActivated()) { + freeAccount = a; + break; + } + } + if (freeAccount >= 0) { + presentFragment(new LoginActivity(freeAccount)); + } + }), + new SearchResult(503, LocaleController.getString("UserBio", R.string.UserBio), 0, () -> { + if (userInfo != null) { + presentFragment(new ChangeBioActivity()); + } + }), + + new SearchResult(1, LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsSettingsActivity())), + new SearchResult(2, LocaleController.getString("NotificationsPrivateChats", R.string.NotificationsPrivateChats), LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsCustomSettingsActivity(NotificationsController.TYPE_PRIVATE, new ArrayList<>(), true))), + new SearchResult(3, LocaleController.getString("NotificationsGroups", R.string.NotificationsGroups), LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsCustomSettingsActivity(NotificationsController.TYPE_GROUP, new ArrayList<>(), true))), + new SearchResult(4, LocaleController.getString("NotificationsChannels", R.string.NotificationsChannels), LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsCustomSettingsActivity(NotificationsController.TYPE_CHANNEL, new ArrayList<>(), true))), + new SearchResult(5, LocaleController.getString("VoipNotificationSettings", R.string.VoipNotificationSettings), "callsSectionRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsSettingsActivity())), + new SearchResult(6, LocaleController.getString("BadgeNumber", R.string.BadgeNumber), "badgeNumberSection", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsSettingsActivity())), + new SearchResult(7, LocaleController.getString("InAppNotifications", R.string.InAppNotifications), "inappSectionRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsSettingsActivity())), + new SearchResult(8, LocaleController.getString("ContactJoined", R.string.ContactJoined), "contactJoinedRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsSettingsActivity())), + new SearchResult(9, LocaleController.getString("PinnedMessages", R.string.PinnedMessages), "pinnedMessageRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsSettingsActivity())), + new SearchResult(10, LocaleController.getString("ResetAllNotifications", R.string.ResetAllNotifications), "resetNotificationsRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.menu_notifications, () -> presentFragment(new NotificationsSettingsActivity())), + + new SearchResult(100, LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())), + new SearchResult(101, LocaleController.getString("BlockedUsers", R.string.BlockedUsers), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacyUsersActivity())), + new SearchResult(105, LocaleController.getString("PrivacyPhone", R.string.PrivacyPhone), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_PHONE, true))), + new SearchResult(102, LocaleController.getString("PrivacyLastSeen", R.string.PrivacyLastSeen), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_LASTSEEN, true))), + new SearchResult(103, LocaleController.getString("PrivacyProfilePhoto", R.string.PrivacyProfilePhoto), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_PHOTO, true))), + new SearchResult(104, LocaleController.getString("PrivacyForwards", R.string.PrivacyForwards), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_FORWARDS, true))), + new SearchResult(105, LocaleController.getString("PrivacyP2P", R.string.PrivacyP2P), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_P2P, true))), + new SearchResult(106, LocaleController.getString("Calls", R.string.Calls), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_CALLS, true))), + new SearchResult(107, LocaleController.getString("GroupsAndChannels", R.string.GroupsAndChannels), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_INVITE, true))), + new SearchResult(108, LocaleController.getString("Passcode", R.string.Passcode), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PasscodeActivity(SharedConfig.passcodeHash.length() > 0 ? 2 : 0))), + new SearchResult(109, LocaleController.getString("TwoStepVerification", R.string.TwoStepVerification), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new TwoStepVerificationActivity())), + new SearchResult(110, LocaleController.getString("SessionsTitle", R.string.SessionsTitle), R.drawable.menu_secret, () -> presentFragment(new SessionsActivity(0))), + getMessagesController().autoarchiveAvailable ? new SearchResult(121, LocaleController.getString("ArchiveAndMute", R.string.ArchiveAndMute), "newChatsRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())) : null, + new SearchResult(112, LocaleController.getString("DeleteAccountIfAwayFor2", R.string.DeleteAccountIfAwayFor2), "deleteAccountRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())), + new SearchResult(113, LocaleController.getString("PrivacyPaymentsClear", R.string.PrivacyPaymentsClear), "paymentsClearRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())), + new SearchResult(114, LocaleController.getString("WebSessionsTitle", R.string.WebSessionsTitle), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new SessionsActivity(1))), + new SearchResult(115, LocaleController.getString("SyncContactsDelete", R.string.SyncContactsDelete), "contactsDeleteRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())), + new SearchResult(116, LocaleController.getString("SyncContacts", R.string.SyncContacts), "contactsSyncRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())), + new SearchResult(117, LocaleController.getString("SuggestContacts", R.string.SuggestContacts), "contactsSuggestRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())), + new SearchResult(118, LocaleController.getString("MapPreviewProvider", R.string.MapPreviewProvider), "secretMapRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())), + new SearchResult(119, LocaleController.getString("SecretWebPage", R.string.SecretWebPage), "secretWebpageRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.menu_secret, () -> presentFragment(new PrivacySettingsActivity())), + new SearchResult(120, LocaleController.getString("Devices", R.string.Devices), R.drawable.menu_secret, () -> presentFragment(new SessionsActivity(0))), + + new SearchResult(200, LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(201, LocaleController.getString("DataUsage", R.string.DataUsage), "usageSectionRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(202, LocaleController.getString("StorageUsage", R.string.StorageUsage), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new CacheControlActivity())), + new SearchResult(203, LocaleController.getString("KeepMedia", R.string.KeepMedia), "keepMediaRow", LocaleController.getString("DataSettings", R.string.DataSettings), LocaleController.getString("StorageUsage", R.string.StorageUsage), R.drawable.menu_data, () -> presentFragment(new CacheControlActivity())), + new SearchResult(204, LocaleController.getString("ClearMediaCache", R.string.ClearMediaCache), "cacheRow", LocaleController.getString("DataSettings", R.string.DataSettings), LocaleController.getString("StorageUsage", R.string.StorageUsage), R.drawable.menu_data, () -> presentFragment(new CacheControlActivity())), + new SearchResult(205, LocaleController.getString("LocalDatabase", R.string.LocalDatabase), "databaseRow", LocaleController.getString("DataSettings", R.string.DataSettings), LocaleController.getString("StorageUsage", R.string.StorageUsage), R.drawable.menu_data, () -> presentFragment(new CacheControlActivity())), + new SearchResult(206, LocaleController.getString("NetworkUsage", R.string.NetworkUsage), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataUsageActivity())), + new SearchResult(207, LocaleController.getString("AutomaticMediaDownload", R.string.AutomaticMediaDownload), "mediaDownloadSectionRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(208, LocaleController.getString("WhenUsingMobileData", R.string.WhenUsingMobileData), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataAutoDownloadActivity(0))), + new SearchResult(209, LocaleController.getString("WhenConnectedOnWiFi", R.string.WhenConnectedOnWiFi), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataAutoDownloadActivity(1))), + new SearchResult(210, LocaleController.getString("WhenRoaming", R.string.WhenRoaming), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataAutoDownloadActivity(2))), + new SearchResult(211, LocaleController.getString("ResetAutomaticMediaDownload", R.string.ResetAutomaticMediaDownload), "resetDownloadRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(212, LocaleController.getString("AutoplayMedia", R.string.AutoplayMedia), "autoplayHeaderRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(213, LocaleController.getString("AutoplayGIF", R.string.AutoplayGIF), "autoplayGifsRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(214, LocaleController.getString("AutoplayVideo", R.string.AutoplayVideo), "autoplayVideoRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(215, LocaleController.getString("Streaming", R.string.Streaming), "streamSectionRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(216, LocaleController.getString("EnableStreaming", R.string.EnableStreaming), "enableStreamRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(217, LocaleController.getString("Calls", R.string.Calls), "callsSectionRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(218, LocaleController.getString("VoipUseLessData", R.string.VoipUseLessData), "useLessDataForCallsRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(219, LocaleController.getString("VoipQuickReplies", R.string.VoipQuickReplies), "quickRepliesRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + new SearchResult(220, LocaleController.getString("ProxySettings", R.string.ProxySettings), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new ProxyListActivity())), + new SearchResult(221, LocaleController.getString("UseProxyForCalls", R.string.UseProxyForCalls), "callsRow", LocaleController.getString("DataSettings", R.string.DataSettings), LocaleController.getString("ProxySettings", R.string.ProxySettings), R.drawable.menu_data, () -> presentFragment(new ProxyListActivity())), + new SearchResult(111, LocaleController.getString("PrivacyDeleteCloudDrafts", R.string.PrivacyDeleteCloudDrafts), "clearDraftsRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.menu_data, () -> presentFragment(new DataSettingsActivity())), + + new SearchResult(300, LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(301, LocaleController.getString("TextSizeHeader", R.string.TextSizeHeader), "textSizeHeaderRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(302, LocaleController.getString("ChatBackground", R.string.ChatBackground), LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new WallpapersListActivity(WallpapersListActivity.TYPE_ALL))), + new SearchResult(303, LocaleController.getString("SetColor", R.string.SetColor), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("ChatBackground", R.string.ChatBackground), R.drawable.menu_chats, () -> presentFragment(new WallpapersListActivity(WallpapersListActivity.TYPE_COLOR))), + new SearchResult(304, LocaleController.getString("ResetChatBackgrounds", R.string.ResetChatBackgrounds), "resetRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("ChatBackground", R.string.ChatBackground), R.drawable.menu_chats, () -> presentFragment(new WallpapersListActivity(WallpapersListActivity.TYPE_ALL))), + new SearchResult(305, LocaleController.getString("AutoNightTheme", R.string.AutoNightTheme), LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_NIGHT))), + new SearchResult(306, LocaleController.getString("ColorTheme", R.string.ColorTheme), "themeHeaderRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(307, LocaleController.getString("ChromeCustomTabs", R.string.ChromeCustomTabs), "customTabsRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(308, LocaleController.getString("DirectShare", R.string.DirectShare), "directShareRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(309, LocaleController.getString("EnableAnimations", R.string.EnableAnimations), "enableAnimationsRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(310, LocaleController.getString("RaiseToSpeak", R.string.RaiseToSpeak), "raiseToSpeakRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(311, LocaleController.getString("SendByEnter", R.string.SendByEnter), "sendByEnterRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(312, LocaleController.getString("SaveToGallerySettings", R.string.SaveToGallerySettings), "saveToGalleryRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(312, LocaleController.getString("DistanceUnits", R.string.DistanceUnits), "distanceRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), + new SearchResult(313, LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.menu_chats, () -> presentFragment(new StickersActivity(MediaDataController.TYPE_IMAGE))), + new SearchResult(314, LocaleController.getString("SuggestStickers", R.string.SuggestStickers), "suggestRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.menu_chats, () -> presentFragment(new StickersActivity(MediaDataController.TYPE_IMAGE))), + new SearchResult(315, LocaleController.getString("FeaturedStickers", R.string.FeaturedStickers), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.menu_chats, () -> presentFragment(new FeaturedStickersActivity())), + new SearchResult(316, LocaleController.getString("Masks", R.string.Masks), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.menu_chats, () -> presentFragment(new StickersActivity(MediaDataController.TYPE_MASK))), + new SearchResult(317, LocaleController.getString("ArchivedStickers", R.string.ArchivedStickers), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.menu_chats, () -> presentFragment(new ArchivedStickersActivity(MediaDataController.TYPE_IMAGE))), + new SearchResult(317, LocaleController.getString("ArchivedMasks", R.string.ArchivedMasks), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.menu_chats, () -> presentFragment(new ArchivedStickersActivity(MediaDataController.TYPE_MASK))), + + new SearchResult(400, LocaleController.getString("Language", R.string.Language), R.drawable.menu_language, () -> presentFragment(new LanguageSelectActivity())), + + new SearchResult(402, LocaleController.getString("AskAQuestion", R.string.AskAQuestion), LocaleController.getString("SettingsHelp", R.string.SettingsHelp), R.drawable.menu_help, () -> showDialog(AlertsCreator.createSupportAlert(ProfileActivity.this))), + new SearchResult(403, LocaleController.getString("TelegramFAQ", R.string.TelegramFAQ), LocaleController.getString("SettingsHelp", R.string.SettingsHelp), R.drawable.menu_help, () -> Browser.openUrl(getParentActivity(), LocaleController.getString("TelegramFaqUrl", R.string.TelegramFaqUrl))), + new SearchResult(404, LocaleController.getString("PrivacyPolicy", R.string.PrivacyPolicy), LocaleController.getString("SettingsHelp", R.string.SettingsHelp), R.drawable.menu_help, () -> Browser.openUrl(getParentActivity(), LocaleController.getString("PrivacyPolicyUrl", R.string.PrivacyPolicyUrl))), + }; + private ArrayList faqSearchArray = new ArrayList<>(); + + private Context mContext; + private ArrayList resultNames = new ArrayList<>(); + private ArrayList searchResults = new ArrayList<>(); + private ArrayList faqSearchResults = new ArrayList<>(); + private ArrayList recentSearches = new ArrayList<>(); + private boolean searchWas; + private Runnable searchRunnable; + private String lastSearchString; + private TLRPC.WebPage faqWebPage; + private boolean loadingFaqPage; + + public SearchAdapter(Context context) { + mContext = context; + + HashMap resultHashMap = new HashMap<>(); + for (int a = 0; a < searchArray.length; a++) { + if (searchArray[a] == null) { + continue; + } + resultHashMap.put(searchArray[a].guid, searchArray[a]); + } + Set set = MessagesController.getGlobalMainSettings().getStringSet("settingsSearchRecent2", null); + if (set != null) { + for (String value : set) { + try { + SerializedData data = new SerializedData(Utilities.hexToBytes(value)); + int num = data.readInt32(false); + int type = data.readInt32(false); + if (type == 0) { + String title = data.readString(false); + int count = data.readInt32(false); + String[] path = null; + if (count > 0) { + path = new String[count]; + for (int a = 0; a < count; a++) { + path[a] = data.readString(false); + } + } + String url = data.readString(false); + MessagesController.FaqSearchResult result = new MessagesController.FaqSearchResult(title, path, url); + result.num = num; + recentSearches.add(result); + } else if (type == 1) { + SearchResult result = resultHashMap.get(data.readInt32(false)); + if (result != null) { + result.num = num; + recentSearches.add(result); + } + } + } catch (Exception ignore) { + + } + } + } + Collections.sort(recentSearches, (o1, o2) -> { + int n1 = getNum(o1); + int n2 = getNum(o2); + if (n1 < n2) { + return -1; + } else if (n1 > n2) { + return 1; + } + return 0; + }); + } + + private void loadFaqWebPage() { + faqWebPage = getMessagesController().faqWebPage; + if (faqWebPage != null) { + faqSearchArray.addAll(getMessagesController().faqSearchArray); + } + if (faqWebPage != null || loadingFaqPage) { + return; + } + loadingFaqPage = true; + final TLRPC.TL_messages_getWebPage req2 = new TLRPC.TL_messages_getWebPage(); + req2.url = LocaleController.getString("TelegramFaqUrl", R.string.TelegramFaqUrl); + req2.hash = 0; + getConnectionsManager().sendRequest(req2, (response2, error2) -> { + if (response2 instanceof TLRPC.WebPage) { + ArrayList arrayList = new ArrayList<>(); + TLRPC.WebPage page = (TLRPC.WebPage) response2; + if (page.cached_page != null) { + for (int a = 0, N = page.cached_page.blocks.size(); a < N; a++) { + TLRPC.PageBlock block = page.cached_page.blocks.get(a); + if (block instanceof TLRPC.TL_pageBlockList) { + String paragraph = null; + if (a != 0) { + TLRPC.PageBlock prevBlock = page.cached_page.blocks.get(a - 1); + if (prevBlock instanceof TLRPC.TL_pageBlockParagraph) { + TLRPC.TL_pageBlockParagraph pageBlockParagraph = (TLRPC.TL_pageBlockParagraph) prevBlock; + paragraph = ArticleViewer.getPlainText(pageBlockParagraph.text).toString(); + } + } + TLRPC.TL_pageBlockList list = (TLRPC.TL_pageBlockList) block; + for (int b = 0, N2 = list.items.size(); b < N2; b++) { + TLRPC.PageListItem item = list.items.get(b); + if (item instanceof TLRPC.TL_pageListItemText) { + TLRPC.TL_pageListItemText itemText = (TLRPC.TL_pageListItemText) item; + String url = ArticleViewer.getUrl(itemText.text); + String text = ArticleViewer.getPlainText(itemText.text).toString(); + if (TextUtils.isEmpty(url) || TextUtils.isEmpty(text)) { + continue; + } + String[] path; + if (paragraph != null) { + path = new String[]{LocaleController.getString("SettingsSearchFaq", R.string.SettingsSearchFaq), paragraph}; + } else { + path = new String[]{LocaleController.getString("SettingsSearchFaq", R.string.SettingsSearchFaq)}; + } + arrayList.add(new MessagesController.FaqSearchResult(text, path, url)); + } + } + } else if (block instanceof TLRPC.TL_pageBlockAnchor) { + break; + } + } + faqWebPage = page; + } + AndroidUtilities.runOnUIThread(() -> { + faqSearchArray.addAll(arrayList); + getMessagesController().faqSearchArray = arrayList; + getMessagesController().faqWebPage = faqWebPage; + if (!searchWas) { + notifyDataSetChanged(); + } + }); + } + loadingFaqPage = false; + }); + } + + @Override + public int getItemCount() { + if (searchWas) { + return searchResults.size() + (faqSearchResults.isEmpty() ? 0 : 1 + faqSearchResults.size()); + } + return (recentSearches.isEmpty() ? 0 : recentSearches.size() + 1) + (faqSearchArray.isEmpty() ? 0 : faqSearchArray.size() + 1); + } + + @Override + public boolean isEnabled(RecyclerView.ViewHolder holder) { + return holder.getItemViewType() == 0; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + switch (holder.getItemViewType()) { + case 0: { + SettingsSearchCell searchCell = (SettingsSearchCell) holder.itemView; + if (searchWas) { + if (position < searchResults.size()) { + SearchResult result = searchResults.get(position); + SearchResult prevResult = position > 0 ? searchResults.get(position - 1) : null; + int icon; + if (prevResult != null && prevResult.iconResId == result.iconResId) { + icon = 0; + } else { + icon = result.iconResId; + } + searchCell.setTextAndValueAndIcon(resultNames.get(position), result.path, icon, position < searchResults.size() - 1); + } else { + position -= searchResults.size() + 1; + MessagesController.FaqSearchResult result = faqSearchResults.get(position); + searchCell.setTextAndValue(resultNames.get(position + searchResults.size()), result.path, true, position < searchResults.size() - 1); + } + } else { + if (!recentSearches.isEmpty()) { + position--; + } + if (position < recentSearches.size()) { + Object object = recentSearches.get(position); + if (object instanceof SearchResult) { + SearchResult result = (SearchResult) object; + searchCell.setTextAndValue(result.searchTitle, result.path, false, position < recentSearches.size() - 1); + } else if (object instanceof MessagesController.FaqSearchResult) { + MessagesController.FaqSearchResult result = (MessagesController.FaqSearchResult) object; + searchCell.setTextAndValue(result.title, result.path, true, position < recentSearches.size() - 1); + } + } else { + position -= recentSearches.size() + 1; + MessagesController.FaqSearchResult result = faqSearchArray.get(position); + searchCell.setTextAndValue(result.title, result.path, true, position < recentSearches.size() - 1); + } + } + break; + } + case 1: { + GraySectionCell sectionCell = (GraySectionCell) holder.itemView; + sectionCell.setText(LocaleController.getString("SettingsFaqSearchTitle", R.string.SettingsFaqSearchTitle)); + break; + } + case 2: { + HeaderCell headerCell = (HeaderCell) holder.itemView; + headerCell.setText(LocaleController.getString("SettingsRecent", R.string.SettingsRecent)); + break; + } + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view; + switch (viewType) { + case 0: + view = new SettingsSearchCell(mContext); + break; + case 1: + view = new GraySectionCell(mContext); + break; + case 2: + default: + view = new HeaderCell(mContext, 16); + break; + } + view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); + return new RecyclerListView.Holder(view); + } + + @Override + public int getItemViewType(int position) { + if (searchWas) { + if (position < searchResults.size()) { + return 0; + } else if (position == searchResults.size()) { + return 1; + } + } else { + if (position == 0) { + if (!recentSearches.isEmpty()) { + return 2; + } else { + return 1; + } + } else if (!recentSearches.isEmpty() && position == recentSearches.size() + 1) { + return 1; + } + } + return 0; + } + + public void addRecent(Object object) { + int index = recentSearches.indexOf(object); + if (index >= 0) { + recentSearches.remove(index); + } + recentSearches.add(0, object); + if (!searchWas) { + notifyDataSetChanged(); + } + if (recentSearches.size() > 20) { + recentSearches.remove(recentSearches.size() - 1); + } + LinkedHashSet toSave = new LinkedHashSet<>(); + for (int a = 0, N = recentSearches.size(); a < N; a++) { + Object o = recentSearches.get(a); + if (o instanceof SearchResult) { + ((SearchResult) o).num = a; + } else if (o instanceof MessagesController.FaqSearchResult) { + ((MessagesController.FaqSearchResult) o).num = a; + } + toSave.add(o.toString()); + } + MessagesController.getGlobalMainSettings().edit().putStringSet("settingsSearchRecent2", toSave).commit(); + } + + public void clearRecent() { + recentSearches.clear(); + MessagesController.getGlobalMainSettings().edit().remove("settingsSearchRecent2").commit(); + notifyDataSetChanged(); + } + + private int getNum(Object o) { + if (o instanceof SearchResult) { + return ((SearchResult) o).num; + } else if (o instanceof MessagesController.FaqSearchResult) { + return ((MessagesController.FaqSearchResult) o).num; + } + return 0; + } + + public void search(String text) { + lastSearchString = text; + if (searchRunnable != null) { + Utilities.searchQueue.cancelRunnable(searchRunnable); + searchRunnable = null; + } + if (TextUtils.isEmpty(text)) { + searchWas = false; + searchResults.clear(); + faqSearchResults.clear(); + resultNames.clear(); + emptyView.setTopImage(0); + emptyView.setText(LocaleController.getString("SettingsNoRecent", R.string.SettingsNoRecent)); + notifyDataSetChanged(); + return; + } + Utilities.searchQueue.postRunnable(searchRunnable = () -> { + ArrayList results = new ArrayList<>(); + ArrayList faqResults = new ArrayList<>(); + ArrayList names = new ArrayList<>(); + String[] searchArgs = text.split(" "); + String[] translitArgs = new String[searchArgs.length]; + for (int a = 0; a < searchArgs.length; a++) { + translitArgs[a] = LocaleController.getInstance().getTranslitString(searchArgs[a]); + if (translitArgs[a].equals(searchArgs[a])) { + translitArgs[a] = null; + } + } + + for (int a = 0; a < searchArray.length; a++) { + SearchResult result = searchArray[a]; + if (result == null) { + continue; + } + String title = " " + result.searchTitle.toLowerCase(); + SpannableStringBuilder stringBuilder = null; + for (int i = 0; i < searchArgs.length; i++) { + if (searchArgs[i].length() != 0) { + String searchString = searchArgs[i]; + int index = title.indexOf(" " + searchString); + if (index < 0 && translitArgs[i] != null) { + searchString = translitArgs[i]; + index = title.indexOf(" " + searchString); + } + if (index >= 0) { + if (stringBuilder == null) { + stringBuilder = new SpannableStringBuilder(result.searchTitle); + } + stringBuilder.setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4)), index, index + searchString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + break; + } + } + if (stringBuilder != null && i == searchArgs.length - 1) { + if (result.guid == 502) { + int freeAccount = -1; + for (int b = 0; b < UserConfig.MAX_ACCOUNT_COUNT; b++) { + if (!UserConfig.getInstance(a).isClientActivated()) { + freeAccount = b; + break; + } + } + if (freeAccount < 0) { + continue; + } + } + results.add(result); + names.add(stringBuilder); + } + } + } + if (faqWebPage != null) { + for (int a = 0, N = faqSearchArray.size(); a < N; a++) { + MessagesController.FaqSearchResult result = faqSearchArray.get(a); + String title = " " + result.title.toLowerCase(); + SpannableStringBuilder stringBuilder = null; + for (int i = 0; i < searchArgs.length; i++) { + if (searchArgs[i].length() != 0) { + String searchString = searchArgs[i]; + int index = title.indexOf(" " + searchString); + if (index < 0 && translitArgs[i] != null) { + searchString = translitArgs[i]; + index = title.indexOf(" " + searchString); + } + if (index >= 0) { + if (stringBuilder == null) { + stringBuilder = new SpannableStringBuilder(result.title); + } + stringBuilder.setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4)), index, index + searchString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + break; + } + } + if (stringBuilder != null && i == searchArgs.length - 1) { + faqResults.add(result); + names.add(stringBuilder); + } + } + } + } + + AndroidUtilities.runOnUIThread(() -> { + if (!text.equals(lastSearchString)) { + return; + } + if (!searchWas) { + emptyView.setTopImage(R.drawable.settings_noresults); + emptyView.setText(LocaleController.getString("SettingsNoResults", R.string.SettingsNoResults)); + } + searchWas = true; + searchResults = results; + faqSearchResults = faqResults; + resultNames = names; + notifyDataSetChanged(); + }); + }, 300); + } + + public boolean isSearchWas() { + return searchWas; + } + } + @Override public ArrayList getThemeDescriptions() { ThemeDescription.ThemeDescriptionDelegate themeDelegate = () -> { @@ -4947,9 +7026,11 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } if (!isPulledDown) { - final Object onlineTextViewTag = onlineTextView[1].getTag(); - if (onlineTextViewTag instanceof String) { - onlineTextView[1].setTextColor(Theme.getColor((String) onlineTextViewTag)); + if (onlineTextView[1] != null) { + final Object onlineTextViewTag = onlineTextView[1].getTag(); + if (onlineTextViewTag instanceof String) { + onlineTextView[1].setTextColor(Theme.getColor((String) onlineTextViewTag)); + } } if (lockIconDrawable != null) { lockIconDrawable.setColorFilter(Theme.getColor(Theme.key_chat_lockIcon), PorterDuff.Mode.SRC_IN); @@ -4970,6 +7051,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } arrayList.add(new ThemeDescription(listView, 0, null, null, null, null, Theme.key_windowBackgroundWhite)); + arrayList.add(new ThemeDescription(searchListView, 0, null, null, null, null, Theme.key_windowBackgroundWhite)); arrayList.add(new ThemeDescription(listView, 0, null, null, null, null, Theme.key_windowBackgroundGray)); arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUBACKGROUND, null, null, null, null, Theme.key_actionBarDefaultSubmenuBackground)); arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUITEM, null, null, null, null, Theme.key_actionBarDefaultSubmenuItem)); @@ -5042,12 +7124,19 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. arrayList.add(new ThemeDescription(listView, 0, new Class[]{AboutLinkCell.class}, Theme.linkSelectionPaint, null, null, Theme.key_windowBackgroundWhiteLinkSelection)); arrayList.add(new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); - arrayList.add(new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGray)); arrayList.add(new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); - arrayList.add(new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGray)); arrayList.add(new ThemeDescription(listView, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4)); + arrayList.add(new ThemeDescription(searchListView, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); + + arrayList.add(new ThemeDescription(searchListView, 0, new Class[]{GraySectionCell.class}, new String[]{"textView"}, null, null, null, Theme.key_graySectionText)); + arrayList.add(new ThemeDescription(searchListView, ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{GraySectionCell.class}, null, null, null, Theme.key_graySection)); + + arrayList.add(new ThemeDescription(searchListView, 0, new Class[]{SettingsSearchCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(searchListView, 0, new Class[]{SettingsSearchCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2)); + arrayList.add(new ThemeDescription(searchListView, 0, new Class[]{SettingsSearchCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon)); + if (mediaHeaderVisible) { arrayList.add(new ThemeDescription(nameTextView[1], 0, null, null, new Drawable[]{verifiedCheckDrawable}, null, Theme.key_player_actionBarTitle)); arrayList.add(new ThemeDescription(nameTextView[1], 0, null, null, new Drawable[]{verifiedDrawable}, null, Theme.key_windowBackgroundWhite)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java index 767d7f461..e52f38d23 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java @@ -462,7 +462,7 @@ public class ProfileNotificationsActivity extends BaseFragment implements Notifi if (adapter != null) { adapter.notifyItemChanged(smartRow); } - dismissCurrentDialig(); + dismissCurrentDialog(); }); AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setTitle(LocaleController.getString("SmartNotificationsAlert", R.string.SmartNotificationsAlert)); @@ -474,7 +474,7 @@ public class ProfileNotificationsActivity extends BaseFragment implements Notifi if (adapter != null) { adapter.notifyItemChanged(smartRow); } - dismissCurrentDialig(); + dismissCurrentDialog(); }); showDialog(builder.create()); } else if (position == colorRow) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java deleted file mode 100644 index 1cb68fe2c..000000000 --- a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java +++ /dev/null @@ -1,2446 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 5.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-2018. - */ - -package org.telegram.ui; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.StateListAnimator; -import android.animation.ValueAnimator; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Outline; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.style.ForegroundColorSpan; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; -import android.view.ViewTreeObserver; -import android.view.WindowManager; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.Keep; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import org.telegram.PhoneFormat.PhoneFormat; -import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.ApplicationLoader; -import org.telegram.messenger.BuildConfig; -import org.telegram.messenger.BuildVars; -import org.telegram.messenger.ContactsController; -import org.telegram.messenger.FileLoader; -import org.telegram.messenger.FileLog; -import org.telegram.messenger.ImageLoader; -import org.telegram.messenger.ImageLocation; -import org.telegram.messenger.LocaleController; -import org.telegram.messenger.MediaDataController; -import org.telegram.messenger.MessageObject; -import org.telegram.messenger.MessagesController; -import org.telegram.messenger.MessagesStorage; -import org.telegram.messenger.NotificationCenter; -import org.telegram.messenger.NotificationsController; -import org.telegram.messenger.R; -import org.telegram.messenger.SharedConfig; -import org.telegram.messenger.UserConfig; -import org.telegram.messenger.UserObject; -import org.telegram.messenger.Utilities; -import org.telegram.messenger.browser.Browser; -import org.telegram.tgnet.ConnectionsManager; -import org.telegram.tgnet.SerializedData; -import org.telegram.tgnet.TLRPC; -import org.telegram.ui.ActionBar.ActionBar; -import org.telegram.ui.ActionBar.ActionBarMenu; -import org.telegram.ui.ActionBar.ActionBarMenuItem; -import org.telegram.ui.ActionBar.AlertDialog; -import org.telegram.ui.ActionBar.BaseFragment; -import org.telegram.ui.ActionBar.SimpleTextView; -import org.telegram.ui.ActionBar.Theme; -import org.telegram.ui.ActionBar.ThemeDescription; -import org.telegram.ui.Cells.EmptyCell; -import org.telegram.ui.Cells.GraySectionCell; -import org.telegram.ui.Cells.HeaderCell; -import org.telegram.ui.Cells.SettingsSearchCell; -import org.telegram.ui.Cells.ShadowSectionCell; -import org.telegram.ui.Cells.TextCell; -import org.telegram.ui.Cells.TextDetailCell; -import org.telegram.ui.Cells.TextInfoPrivacyCell; -import org.telegram.ui.Components.AlertsCreator; -import org.telegram.ui.Components.AvatarDrawable; -import org.telegram.ui.Components.BackupImageView; -import org.telegram.ui.Components.CombinedDrawable; -import org.telegram.ui.Components.CubicBezierInterpolator; -import org.telegram.ui.Components.EmptyTextProgressView; -import org.telegram.ui.Components.ImageUpdater; -import org.telegram.ui.Components.LayoutHelper; -import org.telegram.ui.Components.RadialProgressView; -import org.telegram.ui.Components.RecyclerListView; -import org.telegram.ui.Components.voip.VoIPHelper; - -import java.io.File; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Set; - -import cn.hutool.core.util.RuntimeUtil; -import kotlin.Unit; -import libv2ray.Libv2ray; -import tw.nekomimi.nekogram.BottomBuilder; -import tw.nekomimi.nekogram.NekoConfig; -import tw.nekomimi.nekogram.NekoXConfig; -import tw.nekomimi.nekogram.NekoXSettingActivity; -import tw.nekomimi.nekogram.parts.UpdateChecksKt; -import tw.nekomimi.nekogram.settings.NekoSettingsActivity; -import tw.nekomimi.nekogram.utils.AlertUtil; -import tw.nekomimi.nekogram.utils.EnvUtil; -import tw.nekomimi.nekogram.utils.FileUtil; -import tw.nekomimi.nekogram.utils.IoUtil; -import tw.nekomimi.nekogram.utils.LangsKt; -import tw.nekomimi.nekogram.utils.ShareUtil; -import tw.nekomimi.nekogram.utils.ThreadUtil; -import tw.nekomimi.nekogram.utils.UIUtil; - -public class SettingsActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate, ImageUpdater.ImageUpdaterDelegate { - - private RecyclerListView listView; - private RecyclerListView searchListView; - private ListAdapter listAdapter; - private SearchAdapter searchAdapter; - private LinearLayoutManager layoutManager; - private FrameLayout avatarContainer; - private BackupImageView avatarImage; - private View avatarOverlay; - private AnimatorSet avatarAnimation; - private RadialProgressView avatarProgressView; - private SimpleTextView titleTextView; - private TextView nameTextView; - private TextView onlineTextView; - private TextView idTextView; - private ImageView writeButton; - private AnimatorSet writeButtonAnimation; - private ImageUpdater imageUpdater; - private AvatarDrawable avatarDrawable; - private TopView topView; - private ActionBarMenuItem otherItem; - private EmptyTextProgressView emptyView; - private ActionBarMenuItem searchItem; - - private TLRPC.FileLocation avatar; - private TLRPC.FileLocation avatarBig; - - private TLRPC.UserFull userInfo; - - private boolean playProfileAnimation; - private boolean allowProfileAnimation = true; - private float animationProgress; - private int initialAnimationExtraHeight; - private boolean openAnimationInProgress; - - private int extraHeight; - private int searchTransitionOffset; - private float searchTransitionProgress; - private Animator searchViewTransition; - private boolean searchMode; - - private int emptyRow; - private int numberSectionRow; - private int numberRow; - private int usernameRow; - private int bioRow; - private int settingsSectionRow; - private int settingsSectionRow2; - private int notificationRow; - private int languageRow; - private int privacyRow; - private int dataRow; - private int chatRow; - private int stickersRow; - private int filtersRow; - private int devicesRow; - private int nekoRow; - private int devicesSectionRow; - private int helpHeaderRow; - private int questionRow; - private int faqRow; - private int policyRow; - private int helpSectionCell; - private int debugHeaderRow; - private int sendLogsRow; - private int clearLogsRow; - private int switchBackendRow; - private int versionRow; - private int rowCount; - - private String currentBio; - private int transitionIndex; - - private final static int edit_name = 1; - private final static int logout = 2; - private final static int search_button = 3; - - private final Interpolator transitionInterpolator = new DecelerateInterpolator(); - - private class TopView extends View { - - private int currentColor; - private Paint paint = new Paint(); - - public TopView(Context context) { - super(context); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), ActionBar.getCurrentActionBarHeight() + (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + AndroidUtilities.dp(91)); - } - - @Override - public void setBackgroundColor(int color) { - if (color != currentColor) { - paint.setColor(color); - invalidate(); - } - } - - @Override - protected void onDraw(Canvas canvas) { - int height = getMeasuredHeight() - AndroidUtilities.dp(91); - canvas.drawRect(0, 0, getMeasuredWidth(), height + extraHeight + searchTransitionOffset, paint); - - if (parentLayout != null) { - parentLayout.drawHeaderShadow(canvas, height + extraHeight + searchTransitionOffset); - } - } - } - - public PhotoViewer.PhotoViewerProvider provider = new PhotoViewer.EmptyPhotoViewerProvider() { - - @Override - public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index, boolean needPreview) { - if (fileLocation == null) { - return null; - } - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(UserConfig.getInstance(currentAccount).getClientUserId()); - if (user != null && user.photo != null && user.photo.photo_big != null) { - TLRPC.FileLocation photoBig = user.photo.photo_big; - if (photoBig.local_id == fileLocation.local_id && photoBig.volume_id == fileLocation.volume_id && photoBig.dc_id == fileLocation.dc_id) { - int[] coords = new int[2]; - avatarImage.getLocationInWindow(coords); - PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); - object.viewX = coords[0]; - object.viewY = coords[1] - (Build.VERSION.SDK_INT >= 21 ? 0 : AndroidUtilities.statusBarHeight); - object.parentView = avatarImage; - object.imageReceiver = avatarImage.getImageReceiver(); - object.dialogId = UserConfig.getInstance(currentAccount).getClientUserId(); - object.thumb = object.imageReceiver.getBitmapSafe(); - object.size = -1; - object.radius = avatarImage.getImageReceiver().getRoundRadius(); - object.scale = avatarContainer.getScaleX(); - return object; - } - } - return null; - } - - @Override - public void willHidePhotoViewer() { - avatarImage.getImageReceiver().setVisible(true, true); - } - }; - - @Override - public boolean onFragmentCreate() { - super.onFragmentCreate(); - - hasOwnBackground = true; - imageUpdater = new ImageUpdater(); - imageUpdater.parentFragment = this; - imageUpdater.delegate = this; - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.updateInterfaces); - NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.userInfoDidLoad); - NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.emojiDidLoad); - - updateRows(); - - getMediaDataController().checkFeaturedStickers(); - getMessagesController().loadSuggestedFilters(); - userInfo = getMessagesController().getUserFull(UserConfig.getInstance(currentAccount).getClientUserId()); - getMessagesController().loadUserInfo(UserConfig.getInstance(currentAccount).getCurrentUser(), true, classGuid); - - return true; - } - - @Override - public void onFragmentDestroy() { - super.onFragmentDestroy(); - if (avatarImage != null) { - avatarImage.setImageDrawable(null); - } - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.updateInterfaces); - NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.userInfoDidLoad); - NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.emojiDidLoad); - imageUpdater.clear(); - } - - @Override - protected ActionBar createActionBar(Context context) { - ActionBar actionBar = new ActionBar(context); - actionBar.setItemsBackgroundColor(Theme.getColor(Theme.key_avatar_actionBarSelectorBlue), false); - actionBar.setItemsColor(Theme.getColor(Theme.key_avatar_actionBarIconBlue), false); - actionBar.setBackButtonImage(R.drawable.ic_ab_back); - actionBar.setCastShadows(false); - actionBar.setAddToContainer(false); - actionBar.setOccupyStatusBar(Build.VERSION.SDK_INT >= 21 && !AndroidUtilities.isTablet()); - return actionBar; - } - - @Override - public View createView(Context context) { - extraHeight = AndroidUtilities.dp(88); - searchTransitionOffset = 0; - searchTransitionProgress = 1f; - searchMode = false; - - actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { - @Override - public void onItemClick(int id) { - if (id == -1) { - finishFragment(); - } else if (id == edit_name) { - presentFragment(new ChangeNameActivity()); - } else if (id == logout) { - presentFragment(new LogoutActivity()); - } - } - }); - ActionBarMenu menu = actionBar.createMenu(); - searchItem = menu.addItem(search_button, R.drawable.ic_ab_search).setIsSearchField(true).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { - - @Override - public Animator getCustomToggleTransition() { - searchMode = !searchMode; - if (searchMode) { - searchAdapter.loadFaqWebPage(); - } else { - searchItem.clearFocusOnSearchView(); - } - return searchExpandTransition(searchMode); - } - - @Override - public void onTextChanged(EditText editText) { - searchAdapter.search(editText.getText().toString().toLowerCase()); - } - }); - searchItem.setContentDescription(LocaleController.getString("SearchInSettings", R.string.SearchInSettings)); - searchItem.setSearchFieldHint(LocaleController.getString("SearchInSettings", R.string.SearchInSettings)); - - otherItem = menu.addItem(0, R.drawable.ic_ab_other); - otherItem.setContentDescription(LocaleController.getString("AccDescrMoreOptions", R.string.AccDescrMoreOptions)); - // otherItem.addSubItem(edit_name, R.drawable.baseline_edit_24, LocaleController.getString("EditName", R.string.EditName)); - otherItem.addSubItem(logout, LocaleController.getString("LogOut", R.string.LogOut)); - - int scrollTo; - int scrollToPosition = 0; - Object writeButtonTag = null; - if (listView != null) { - scrollTo = layoutManager.findFirstVisibleItemPosition(); - View topView = layoutManager.findViewByPosition(scrollTo); - if (topView != null) { - scrollToPosition = topView.getTop(); - } else { - scrollTo = -1; - } - writeButtonTag = writeButton.getTag(); - } else { - scrollTo = -1; - } - - listAdapter = new ListAdapter(context); - searchAdapter = new SearchAdapter(context); - - fragmentView = new FrameLayout(context) { - - private Paint paint = new Paint(); - - @Override - public boolean hasOverlappingRendering() { - return false; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - titleTextView.setTextSize(!AndroidUtilities.isTablet() && getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ? 18 : 20); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (titleTextView != null) { - int textLeft = AndroidUtilities.dp(AndroidUtilities.isTablet() ? 80 : 72); - int textTop = (ActionBar.getCurrentActionBarHeight() - titleTextView.getTextHeight()) / 2 + (Build.VERSION.SDK_INT >= 21 && !AndroidUtilities.isTablet() ? AndroidUtilities.statusBarHeight : 0); - titleTextView.layout(textLeft, textTop, textLeft + titleTextView.getMeasuredWidth(), textTop + titleTextView.getTextHeight()); - } - checkListViewScroll(); - } - - @Override - public void onDraw(Canvas c) { - paint.setColor(Theme.getColor(Theme.key_windowBackgroundWhite)); - View contentView = listView.getVisibility() == View.VISIBLE ? listView : searchListView; - c.drawRect(contentView.getLeft(), contentView.getTop() + extraHeight + searchTransitionOffset, contentView.getRight(), contentView.getBottom(), paint); - } - }; - fragmentView.setWillNotDraw(false); - FrameLayout frameLayout = (FrameLayout) fragmentView; - - listView = new RecyclerListView(context) { - @Override - public boolean hasOverlappingRendering() { - return false; - } - }; - listView.setHideIfEmpty(false); - listView.setVerticalScrollBarEnabled(false); - listView.setLayoutManager(layoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) { - @Override - public boolean supportsPredictiveItemAnimations() { - return false; - } - }); - listView.setGlowColor(Theme.getColor(Theme.key_avatar_backgroundActionBarBlue)); - listView.setPadding(0, AndroidUtilities.dp(88), 0, 0); - frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); - listView.setAdapter(listAdapter); - listView.setItemAnimator(null); - listView.setLayoutAnimation(null); - listView.setClipToPadding(false); - listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { - - private int pressCount = 0; - - @Override - public void onItemClick(View view, int position) { - if (position == notificationRow) { - presentFragment(new NotificationsSettingsActivity()); - } else if (position == privacyRow) { - presentFragment(new PrivacySettingsActivity()); - } else if (position == dataRow) { - presentFragment(new DataSettingsActivity()); - } else if (position == chatRow) { - presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC)); - } else if (position == stickersRow) { - presentFragment(new StickersActivity(MediaDataController.TYPE_IMAGE)); - } else if (position == filtersRow) { - presentFragment(new FiltersSetupActivity()); - } else if (position == devicesRow) { - presentFragment(new SessionsActivity(0)); - } else if (position == nekoRow) { - presentFragment(new NekoSettingsActivity()); - } else if (position == questionRow) { - Browser.openUrl(getParentActivity(), "https://t.me/NekogramX"); - } else if (position == faqRow) { - Browser.openUrl(getParentActivity(), NekoXConfig.FAQ_URL); - } else if (position == policyRow) { - Browser.openUrl(getParentActivity(), LocaleController.getString("PrivacyPolicyUrl", R.string.PrivacyPolicyUrl)); - } else if (position == sendLogsRow) { - sendLogs(); - } else if (position == clearLogsRow) { - AlertDialog pro = AlertUtil.showProgress(getParentActivity()); - pro.show(); - UIUtil.runOnIoDispatcher(() -> { - FileUtil.delete(new File(EnvUtil.getTelegramPath(), "logs")); - ThreadUtil.sleep(100L); - LangsKt.uDismiss(pro); - }); - } else if (position == switchBackendRow) { - if (getParentActivity() == null) { - return; - } - AlertDialog.Builder builder1 = new AlertDialog.Builder(getParentActivity()); - builder1.setMessage(LocaleController.getString("AreYouSure", R.string.AreYouSure)); - builder1.setTitle(LocaleController.getString("AppName", R.string.AppName)); - builder1.setPositiveButton(LocaleController.getString("OK", R.string.OK), (dialogInterface, i) -> { - SharedConfig.pushAuthKey = null; - SharedConfig.pushAuthKeyId = null; - SharedConfig.saveConfig(); - ConnectionsManager.getInstance(currentAccount).switchBackend(); - }); - builder1.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - showDialog(builder1.create()); - } else if (position == languageRow) { - presentFragment(new LanguageSelectActivity()); - } else if (position == usernameRow) { - presentFragment(new ChangeUsernameActivity()); - } else if (position == bioRow) { - if (userInfo != null) { - presentFragment(new ChangeBioActivity()); - } - } else if (position == numberRow) { - presentFragment(new ActionIntroActivity(ActionIntroActivity.ACTION_TYPE_CHANGE_PHONE_NUMBER)); - } else if (position == versionRow) { - TextInfoPrivacyCell cell = (TextInfoPrivacyCell) view; - pressCount++; - if (pressCount == 8) { - NekoXConfig.developerModeEntrance = true; - } - BottomBuilder builder = new BottomBuilder(getParentActivity()); - String message = cell.getTextView().getText().toString(); - try { - if (!BuildVars.isMini) { - message += "\n" + Libv2ray.checkVersionX(); - } - } catch (Exception ignored) { - } - builder.addTitle(message); - String finalMessage = message; - builder.addItem(LocaleController.getString("Copy", R.string.Copy), R.drawable.baseline_content_copy_24, (it) -> { - AndroidUtilities.addToClipboard(finalMessage); - AlertUtil.showToast(LocaleController.getString("TextCopied", R.string.TextCopied)); - return Unit.INSTANCE; - }); - builder.addItem(BuildVars.LOGS_ENABLED ? LocaleController.getString("DebugMenuDisableLogs", R.string.DebugMenuDisableLogs) : LocaleController.getString("DebugMenuEnableLogs", R.string.DebugMenuEnableLogs), R.drawable.baseline_bug_report_24, (it) -> { - BuildVars.LOGS_ENABLED = !BuildVars.LOGS_ENABLED; - SharedPreferences sharedPreferences = ApplicationLoader.applicationContext.getSharedPreferences("systemConfig", Context.MODE_PRIVATE); - sharedPreferences.edit().putBoolean("logsEnabled", BuildVars.LOGS_ENABLED).apply(); - updateRows(); - return Unit.INSTANCE; - }); - if (!BuildVars.isUnknown) { - builder.addItem(LocaleController.getString("CheckUpdate", R.string.CheckUpdate), R.drawable.baseline_system_update_24, (it) -> { - UpdateChecksKt.checkUpdate(getParentActivity()); - return Unit.INSTANCE; - }); - } - if (BuildConfig.BUILD_TYPE.startsWith("release")) { - builder.addItem(LocaleController.getString("SwitchVersion", R.string.SwitchVersion), R.drawable.baseline_replay_24, (it) -> { - Browser.openUrl(getParentActivity(), "https://github.com/NekoX-Dev/NekoX/releases"); - return Unit.INSTANCE; - }); - } - if (NekoXConfig.developerModeEntrance || NekoXConfig.developerMode) { - builder.addItem(LocaleController.getString("DeveloperSettings", R.string.DeveloperSettings), R.drawable.baseline_developer_mode_24, (it) -> { - BottomBuilder devBuilder = new BottomBuilder(getParentActivity()); - devBuilder.addTitle(LocaleController.getString("DevModeTitle", R.string.DevModeTitle), LocaleController.getString("DevModeNotice", R.string.DevModeNotice)); - devBuilder.addItem(LocaleController.getString("Continue", R.string.Continue), R.drawable.baseline_warning_24, true, (__) -> { - presentFragment(new NekoXSettingActivity()); - return Unit.INSTANCE; - }); - devBuilder.addCancelItem(); - devBuilder.show(); - return Unit.INSTANCE; - }); - } - builder.show(); - } - } - }); - - listView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { - - private int pressCount = 0; - - @Override - public boolean onItemClick(View view, int position) { - if (!NekoXConfig.developerMode) return false; - if (position == versionRow) { - pressCount++; - if (pressCount >= 2 || BuildVars.DEBUG_PRIVATE_VERSION) { - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setTitle(LocaleController.getString("DebugMenu", R.string.DebugMenu)); - CharSequence[] items; - items = new CharSequence[]{ - LocaleController.getString("DebugMenuImportContacts", R.string.DebugMenuImportContacts), - LocaleController.getString("DebugMenuReloadContacts", R.string.DebugMenuReloadContacts), - LocaleController.getString("DebugMenuResetContacts", R.string.DebugMenuResetContacts), - LocaleController.getString("DebugMenuResetDialogs", R.string.DebugMenuResetDialogs), - BuildVars.DEBUG_VERSION ? LocaleController.getString("DebugMenuDisableLogs", R.string.DebugMenuDisableLogs) : LocaleController.getString("DebugMenuEnableLogs", R.string.DebugMenuEnableLogs), - null, - LocaleController.getString("DebugMenuClearMediaCache", R.string.DebugMenuClearMediaCache), - LocaleController.getString("DebugMenuCallSettings", R.string.DebugMenuCallSettings), - LocaleController.getString("DebugMenuReadAllDialogs", R.string.DebugMenuReadAllDialogs), - SharedConfig.pauseMusicOnRecord ? LocaleController.getString("DebugMenuDisablePauseMusic", R.string.DebugMenuDisablePauseMusic) : LocaleController.getString("DebugMenuEnablePauseMusic", R.string.DebugMenuEnablePauseMusic) - }; - builder.setItems(items, (dialog, which) -> { - if (which == 0) { - UserConfig.getInstance(currentAccount).syncContacts = true; - UserConfig.getInstance(currentAccount).saveConfig(false); - ContactsController.getInstance(currentAccount).forceImportContacts(); - } else if (which == 1) { - ContactsController.getInstance(currentAccount).loadContacts(false, 0); - } else if (which == 2) { - ContactsController.getInstance(currentAccount).resetImportedContacts(); - } else if (which == 3) { - MessagesController.getInstance(currentAccount).forceResetDialogs(); - } else if (which == 4) { - NekoConfig.toggleResidentNotification(); - } else if (which == 5) { - MessagesStorage.getInstance(currentAccount).clearSentMedia(); - SharedConfig.setNoSoundHintShowed(false); - SharedPreferences.Editor editor = MessagesController.getGlobalMainSettings().edit(); - editor.remove("archivehint").remove("archivehint_l").remove("gifhint").remove("soundHint").remove("themehint").remove("filterhint").apply(); - SharedConfig.textSelectionHintShows = 0; - SharedConfig.lockRecordAudioVideoHint = 0; - SharedConfig.stickersReorderingHintUsed = false; - } else if (which == 6) { - VoIPHelper.showCallDebugSettings(getParentActivity()); - } else if (which == 7) { - MessagesStorage.getInstance(currentAccount).readAllDialogs(-1); - } else if (which == 8) { - SharedConfig.togglePauseMusicOnRecord(); - } - }); - 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(e); - } - } - return true; - } - return false; - } - }); - - - searchListView = new RecyclerListView(context) { - @Override - public boolean hasOverlappingRendering() { - return false; - } - }; - - searchListView.setVerticalScrollBarEnabled(false); - searchListView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); - searchListView.setGlowColor(Theme.getColor(Theme.key_avatar_backgroundActionBarBlue)); - frameLayout.addView(searchListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); - searchListView.setAdapter(searchAdapter); - searchListView.setItemAnimator(null); - searchListView.setLayoutAnimation(null); - searchListView.setOnItemClickListener((view, position) -> { - if (position < 0) { - return; - } - Object object = numberRow; - if (searchAdapter.searchWas) { - if (position < searchAdapter.searchResults.size()) { - object = searchAdapter.searchResults.get(position); - } else { - position -= searchAdapter.searchResults.size() + 1; - if (position >= 0 && position < searchAdapter.faqSearchResults.size()) { - object = searchAdapter.faqSearchResults.get(position); - } - } - } else { - position--; - if (position < 0) { - return; - } - if (position < searchAdapter.recentSearches.size()) { - object = searchAdapter.recentSearches.get(position); - } - } - if (object instanceof SearchAdapter.SearchResult) { - SearchAdapter.SearchResult result = (SearchAdapter.SearchResult) object; - result.open(); - } else if (object instanceof SearchAdapter.FaqSearchResult) { - SearchAdapter.FaqSearchResult result = (SearchAdapter.FaqSearchResult) object; - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.openArticle, searchAdapter.faqWebPage, result.url); - } - if (object != null) { - searchAdapter.addRecent(object); - } - - }); - searchListView.setOnItemLongClickListener((view, position) -> { - if (searchAdapter.isSearchWas()) { - return false; - } - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setTitle(LocaleController.getString("NekoX", R.string.NekoX)); - builder.setMessage(LocaleController.getString("ClearSearch", R.string.ClearSearch)); - builder.setPositiveButton(LocaleController.getString("ClearButton", R.string.ClearButton).toUpperCase(), (dialogInterface, i) -> searchAdapter.clearRecent()); - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - showDialog(builder.create()); - return true; - }); - searchListView.setVisibility(View.GONE); - - emptyView = new EmptyTextProgressView(context); - emptyView.showTextView(); - emptyView.setTextSize(18); - emptyView.setVisibility(View.GONE); - emptyView.setShowAtCenter(true); - emptyView.setPadding(0, AndroidUtilities.dp(50), 0, 0); - frameLayout.addView(emptyView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); - - topView = new TopView(context); - topView.setBackgroundColor(Theme.getColor(Theme.key_avatar_backgroundActionBarBlue)); - frameLayout.addView(topView); - - frameLayout.addView(actionBar); - - avatarContainer = new FrameLayout(context); - avatarContainer.setPivotX(0); - avatarContainer.setPivotY(0); - frameLayout.addView(avatarContainer, LayoutHelper.createFrame(42, 42, Gravity.TOP | Gravity.LEFT, 64, 0, 0, 0)); - avatarContainer.setOnClickListener(v -> { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(UserConfig.getInstance(currentAccount).getClientUserId()); - if (user == null) { - user = UserConfig.getInstance(currentAccount).getCurrentUser(); - } - if (user == null) { - return; - } - imageUpdater.openMenu(user.photo != null && user.photo.photo_big != null && !(user.photo instanceof TLRPC.TL_userProfilePhotoEmpty), () -> MessagesController.getInstance(currentAccount).deleteUserPhoto(null)); - }); - - avatarImage = new BackupImageView(context); - avatarImage.setRoundRadius(AndroidUtilities.dp(21)); - avatarImage.setContentDescription(LocaleController.getString("AccDescrProfilePicture", R.string.AccDescrProfilePicture)); - avatarContainer.addView(avatarImage, LayoutHelper.createFrame(42, 42)); - - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setColor(0x55000000); - - avatarProgressView = new RadialProgressView(context) { - @Override - protected void onDraw(Canvas canvas) { - if (avatarImage != null && avatarImage.getImageReceiver().hasNotThumb()) { - paint.setAlpha((int) (0x55 * avatarImage.getImageReceiver().getCurrentAlpha())); - canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, AndroidUtilities.dp(21), paint); - } - super.onDraw(canvas); - } - }; - avatarProgressView.setSize(AndroidUtilities.dp(26)); - avatarProgressView.setProgressColor(0xffffffff); - avatarContainer.addView(avatarProgressView, LayoutHelper.createFrame(42, 42)); - - showAvatarProgress(false, false); - - titleTextView = new SimpleTextView(context); - titleTextView.setGravity(Gravity.LEFT); - titleTextView.setTextColor(Theme.getColor(Theme.key_actionBarDefaultTitle)); - titleTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - titleTextView.setText(LocaleController.getString("NekoX", R.string.NekoX)); - titleTextView.setAlpha(0.0f); - frameLayout.addView(titleTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP)); - - nameTextView = new TextView(context); - nameTextView.setTextColor(Theme.getColor(Theme.key_profile_title)); - nameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); - nameTextView.setLines(1); - nameTextView.setMaxLines(1); - nameTextView.setSingleLine(true); - nameTextView.setEllipsize(TextUtils.TruncateAt.END); - nameTextView.setGravity(Gravity.LEFT); - nameTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - nameTextView.setPivotX(0); - nameTextView.setPivotY(0); - frameLayout.addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 118, 0, 96, 0)); - - onlineTextView = new TextView(context); - onlineTextView.setTextColor(Theme.getColor(Theme.key_profile_status)); - onlineTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); - onlineTextView.setLines(1); - onlineTextView.setMaxLines(1); - onlineTextView.setSingleLine(true); - onlineTextView.setEllipsize(TextUtils.TruncateAt.END); - onlineTextView.setGravity(Gravity.LEFT); - frameLayout.addView(onlineTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 118, 0, 96, 0)); - - idTextView = new TextView(context); - idTextView.setTextColor(AvatarDrawable.getProfileTextColorForId(5)); - idTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); - idTextView.setLines(1); - idTextView.setMaxLines(1); - idTextView.setSingleLine(true); - idTextView.setEllipsize(TextUtils.TruncateAt.END); - idTextView.setGravity(Gravity.LEFT); - idTextView.setAlpha(1.0f); - frameLayout.addView(idTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 118, 0, 48, 0)); - - writeButton = new ImageView(context); - - Drawable drawable = Theme.createSimpleSelectorCircleDrawable(AndroidUtilities.dp(56), Theme.getColor(Theme.key_profile_actionBackground), Theme.getColor(Theme.key_profile_actionPressedBackground)); - if (Build.VERSION.SDK_INT < 21) { - Drawable shadowDrawable = context.getResources().getDrawable(R.drawable.floating_shadow_profile).mutate(); - shadowDrawable.setColorFilter(new PorterDuffColorFilter(0xff000000, PorterDuff.Mode.SRC_IN)); - CombinedDrawable combinedDrawable = new CombinedDrawable(shadowDrawable, drawable, 0, 0); - combinedDrawable.setIconSize(AndroidUtilities.dp(56), AndroidUtilities.dp(56)); - drawable = combinedDrawable; - } - writeButton.setBackgroundDrawable(drawable); - writeButton.setImageResource(R.drawable.baseline_edit_24); - writeButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_profile_actionIcon), PorterDuff.Mode.SRC_IN)); - writeButton.setScaleType(ImageView.ScaleType.CENTER); - if (Build.VERSION.SDK_INT >= 21) { - StateListAnimator animator = new StateListAnimator(); - animator.addState(new int[]{android.R.attr.state_pressed}, ObjectAnimator.ofFloat(writeButton, "translationZ", AndroidUtilities.dp(2), AndroidUtilities.dp(4)).setDuration(200)); - animator.addState(new int[]{}, ObjectAnimator.ofFloat(writeButton, "translationZ", AndroidUtilities.dp(4), AndroidUtilities.dp(2)).setDuration(200)); - writeButton.setStateListAnimator(animator); - writeButton.setOutlineProvider(new ViewOutlineProvider() { - @SuppressLint("NewApi") - @Override - public void getOutline(View view, Outline outline) { - outline.setOval(0, 0, AndroidUtilities.dp(56), AndroidUtilities.dp(56)); - } - }); - } - frameLayout.addView(writeButton, LayoutHelper.createFrame(Build.VERSION.SDK_INT >= 21 ? 56 : 60, Build.VERSION.SDK_INT >= 21 ? 56 : 60, Gravity.RIGHT | Gravity.TOP, 0, 0, 16, 0)); - writeButton.setOnClickListener(v -> { - presentFragment(new ChangeNameActivity()); - }); - - if (scrollTo != -1) { - layoutManager.scrollToPositionWithOffset(scrollTo, scrollToPosition); - - if (writeButtonTag != null) { - writeButton.setTag(0); - writeButton.setScaleX(0.2f); - writeButton.setScaleY(0.2f); - writeButton.setAlpha(0.0f); - } - } - - needLayout(); - - searchListView.setOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrollStateChanged(RecyclerView recyclerView, int newState) { - if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { - AndroidUtilities.hideKeyboard(getParentActivity().getCurrentFocus()); - } - } - }); - - listView.setOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - checkListViewScroll(); - } - }); - - return fragmentView; - } - - private void updateRows() { - rowCount = 0; - emptyRow = rowCount++; - numberSectionRow = rowCount++; - numberRow = rowCount++; - usernameRow = rowCount++; - bioRow = rowCount++; - settingsSectionRow = rowCount++; - settingsSectionRow2 = rowCount++; - notificationRow = rowCount++; - dataRow = rowCount++; - privacyRow = rowCount++; - chatRow = rowCount++; - stickersRow = rowCount++; - filtersRow = rowCount++; - devicesRow = -1; - nekoRow = rowCount++; - languageRow = rowCount++; - devicesSectionRow = -1; - helpHeaderRow = rowCount++; - questionRow = -1; - faqRow = rowCount++; - policyRow = -1; - if (BuildVars.LOGS_ENABLED) { - helpSectionCell = rowCount++; - debugHeaderRow = rowCount++; - } else { - helpSectionCell = -1; - debugHeaderRow = -1; - } - if (BuildVars.LOGS_ENABLED) { - sendLogsRow = rowCount++; - clearLogsRow = rowCount++; - } else { - sendLogsRow = -1; - clearLogsRow = -1; - } - switchBackendRow = -1; - versionRow = rowCount++; - if (listAdapter != null) { - listAdapter.notifyDataSetChanged(); - } - } - - private Animator searchExpandTransition(boolean enter) { - if (enter) { - getParentActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); - } - if (searchViewTransition != null) { - searchViewTransition.removeAllListeners(); - searchViewTransition.cancel(); - } - ValueAnimator valueAnimator = ValueAnimator.ofFloat(searchTransitionProgress, enter ? 0f : 1f); - int offset = extraHeight; - searchListView.setTranslationY(offset); - searchListView.setVisibility(View.VISIBLE); - searchItem.setVisibility(View.VISIBLE); - - listView.setVisibility(View.VISIBLE); - - needLayout(); - - avatarContainer.setVisibility(View.VISIBLE); - nameTextView.setVisibility(View.VISIBLE); - onlineTextView.setVisibility(View.VISIBLE); - idTextView.setVisibility(View.VISIBLE); - - actionBar.onSearchFieldVisibilityChanged(searchTransitionProgress > 0.5f); - if (otherItem != null) { - otherItem.setVisibility(searchTransitionProgress > 0.5f ? View.VISIBLE : View.GONE); - } - searchItem.setVisibility(searchTransitionProgress > 0.5f ? View.VISIBLE : View.GONE); - - searchItem.getSearchContainer().setVisibility(searchTransitionProgress > 0.5f ? View.GONE : View.VISIBLE); - searchListView.setEmptyView(emptyView); - avatarContainer.setClickable(false); - - valueAnimator.addUpdateListener(animation -> { - searchTransitionProgress = (float) valueAnimator.getAnimatedValue(); - float progressHalf = (searchTransitionProgress - 0.5f) / 0.5f; - float progressHalfEnd = (0.5f - searchTransitionProgress) / 0.5f; - if (progressHalf < 0) { - progressHalf = 0f; - } - if (progressHalfEnd < 0) { - progressHalfEnd = 0f; - } - - searchTransitionOffset = (int) (-offset * (1f - searchTransitionProgress)); - searchListView.setTranslationY(offset * searchTransitionProgress); - emptyView.setTranslationY(offset * searchTransitionProgress); - listView.setTranslationY(-offset * (1f - searchTransitionProgress)); - needLayout(); - - listView.setAlpha(progressHalf); - searchListView.setAlpha(1f - progressHalf); - emptyView.setAlpha(1f - progressHalf); - - avatarContainer.setAlpha(progressHalf); - nameTextView.setAlpha(progressHalf); - onlineTextView.setAlpha(progressHalf); - idTextView.setAlpha(progressHalf); - - searchItem.getSearchField().setAlpha(progressHalfEnd); - if (enter && searchTransitionProgress < 0.7f) { - searchItem.requestFocusOnSearchView(); - } - - searchItem.getSearchContainer().setVisibility(searchTransitionProgress < 0.5f ? View.VISIBLE : View.GONE); - if (otherItem != null) { - otherItem.setVisibility(searchTransitionProgress > 0.5f ? View.VISIBLE : View.GONE); - } - searchItem.setVisibility(searchTransitionProgress > 0.5f ? View.VISIBLE : View.GONE); - - actionBar.onSearchFieldVisibilityChanged(searchTransitionProgress < 0.5f); - - if (otherItem != null) { - otherItem.setAlpha(progressHalf); - } - searchItem.setAlpha(progressHalf); - topView.invalidate(); - - }); - - valueAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - updateSearchViewState(enter); - avatarContainer.setClickable(true); - if (enter) searchItem.requestFocusOnSearchView(); - needLayout(); - } - }); - - valueAnimator.setDuration(180); - valueAnimator.setInterpolator(transitionInterpolator); - searchViewTransition = valueAnimator; - return valueAnimator; - } - - private void updateSearchViewState(boolean enter) { - int hide = enter ? View.GONE : View.VISIBLE; - listView.setVisibility(hide); - searchListView.setVisibility(enter ? View.VISIBLE : View.GONE); - searchItem.getSearchContainer().setVisibility(enter ? View.VISIBLE : View.GONE); - - actionBar.onSearchFieldVisibilityChanged(enter); - - avatarContainer.setVisibility(hide); - nameTextView.setVisibility(hide); - onlineTextView.setVisibility(hide); - idTextView.setVisibility(hide); - - if (otherItem != null) { - otherItem.setAlpha(1f); - otherItem.setVisibility(hide); - } - searchItem.setVisibility(hide); - - avatarContainer.setAlpha(1f); - nameTextView.setAlpha(1f); - onlineTextView.setAlpha(1f); - idTextView.setAlpha(1f); - searchItem.setAlpha(1f); - listView.setAlpha(1f); - searchListView.setAlpha(1f); - emptyView.setAlpha(1f); - if (enter) { - searchListView.setEmptyView(emptyView); - } else { - emptyView.setVisibility(View.GONE); - } - } - - @Override - public void didUploadPhoto(final TLRPC.InputFile file, final TLRPC.PhotoSize bigSize, final TLRPC.PhotoSize smallSize) { - AndroidUtilities.runOnUIThread(() -> { - if (file != null) { - TLRPC.TL_photos_uploadProfilePhoto req = new TLRPC.TL_photos_uploadProfilePhoto(); - req.file = file; - ConnectionsManager.getInstance(currentAccount).sendRequest(req, (response, error) -> { - if (error == null) { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(UserConfig.getInstance(currentAccount).getClientUserId()); - if (user == null) { - user = UserConfig.getInstance(currentAccount).getCurrentUser(); - if (user == null) { - return; - } - MessagesController.getInstance(currentAccount).putUser(user, false); - } else { - UserConfig.getInstance(currentAccount).setCurrentUser(user); - } - TLRPC.TL_photos_photo photo = (TLRPC.TL_photos_photo) response; - ArrayList sizes = photo.photo.sizes; - TLRPC.PhotoSize small = FileLoader.getClosestPhotoSizeWithSize(sizes, 150); - TLRPC.PhotoSize big = FileLoader.getClosestPhotoSizeWithSize(sizes, 800); - user.photo = new TLRPC.TL_userProfilePhoto(); - user.photo.photo_id = photo.photo.id; - if (small != null) { - user.photo.photo_small = small.location; - } - if (big != null) { - user.photo.photo_big = big.location; - } else if (small != null) { - user.photo.photo_small = small.location; - } - - if (photo != null) { - if (small != null && avatar != null) { - File destFile = FileLoader.getPathToAttach(small, true); - File src = FileLoader.getPathToAttach(avatar, true); - src.renameTo(destFile); - String oldKey = avatar.volume_id + "_" + avatar.local_id + "@50_50"; - String newKey = small.location.volume_id + "_" + small.location.local_id + "@50_50"; - ImageLoader.getInstance().replaceImageInCache(oldKey, newKey, ImageLocation.getForUser(user, false), true); - } - if (big != null && avatarBig != null) { - File destFile = FileLoader.getPathToAttach(big, true); - File src = FileLoader.getPathToAttach(avatarBig, true); - src.renameTo(destFile); - } - } - - MessagesStorage.getInstance(currentAccount).clearUserPhotos(user.id); - ArrayList users = new ArrayList<>(); - users.add(user); - MessagesStorage.getInstance(currentAccount).putUsersAndChats(users, null, false, true); - } - AndroidUtilities.runOnUIThread(() -> { - avatar = null; - avatarBig = null; - updateUserData(); - showAvatarProgress(false, true); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.updateInterfaces, MessagesController.UPDATE_MASK_ALL); - NotificationCenter.getInstance(currentAccount).postNotificationName(NotificationCenter.mainUserInfoChanged); - UserConfig.getInstance(currentAccount).saveConfig(true); - }); - }); - } else { - avatar = smallSize.location; - avatarBig = bigSize.location; - avatarImage.setImage(ImageLocation.getForLocal(avatar), "50_50", avatarDrawable, null); - showAvatarProgress(true, false); - } - }); - } - - private void showAvatarProgress(boolean show, boolean animated) { - if (avatarProgressView == null) { - return; - } - if (avatarAnimation != null) { - avatarAnimation.cancel(); - avatarAnimation = null; - } - if (animated) { - avatarAnimation = new AnimatorSet(); - if (show) { - avatarProgressView.setVisibility(View.VISIBLE); - avatarAnimation.playTogether(ObjectAnimator.ofFloat(avatarProgressView, View.ALPHA, 1.0f)); - } else { - avatarAnimation.playTogether(ObjectAnimator.ofFloat(avatarProgressView, View.ALPHA, 0.0f)); - } - avatarAnimation.setDuration(180); - avatarAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (avatarAnimation == null || avatarProgressView == null) { - return; - } - if (!show) { - avatarProgressView.setVisibility(View.INVISIBLE); - } - avatarAnimation = null; - } - - @Override - public void onAnimationCancel(Animator animation) { - avatarAnimation = null; - } - }); - avatarAnimation.start(); - } else { - if (show) { - avatarProgressView.setAlpha(1.0f); - avatarProgressView.setVisibility(View.VISIBLE); - } else { - avatarProgressView.setAlpha(0.0f); - avatarProgressView.setVisibility(View.INVISIBLE); - } - } - } - - @Override - public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { - imageUpdater.onActivityResult(requestCode, resultCode, data); - } - - @Override - public void saveSelfArgs(Bundle args) { - if (imageUpdater != null && imageUpdater.currentPicturePath != null) { - args.putString("path", imageUpdater.currentPicturePath); - } - } - - @Override - public void restoreSelfArgs(Bundle args) { - if (imageUpdater != null) { - imageUpdater.currentPicturePath = args.getString("path"); - } - } - - @Override - public void didReceivedNotification(int id, int account, Object... args) { - if (id == NotificationCenter.updateInterfaces) { - int mask = (Integer) args[0]; - if ((mask & MessagesController.UPDATE_MASK_AVATAR) != 0 || (mask & MessagesController.UPDATE_MASK_NAME) != 0) { - updateUserData(); - } - } else if (id == NotificationCenter.userInfoDidLoad) { - Integer uid = (Integer) args[0]; - if (uid == UserConfig.getInstance(currentAccount).getClientUserId() && listAdapter != null) { - userInfo = (TLRPC.UserFull) args[1]; - if (!TextUtils.equals(userInfo.about, currentBio)) { - listAdapter.notifyItemChanged(bioRow); - } - } - } else if (id == NotificationCenter.emojiDidLoad) { - if (listView != null) { - listView.invalidateViews(); - } - } - } - - @Override - public void onResume() { - super.onResume(); - if (listAdapter != null) { - listAdapter.notifyDataSetChanged(); - } - updateUserData(); - fixLayout(); - setParentActivityTitle(LocaleController.getString("Settings", R.string.Settings)); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - fixLayout(); - } - - @Override - protected void onTransitionAnimationStart(boolean isOpen, boolean backward) { - if ((!isOpen && backward || isOpen && !backward) && playProfileAnimation && allowProfileAnimation) { - openAnimationInProgress = true; - } - if (isOpen) { - transitionIndex = NotificationCenter.getInstance(currentAccount).setAnimationInProgress(transitionIndex, new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoad, NotificationCenter.mediaCountsDidLoad, NotificationCenter.userInfoDidLoad}); - } - } - - @Override - protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) { - if (isOpen) { - if (!backward && playProfileAnimation && allowProfileAnimation) { - openAnimationInProgress = false; - } - NotificationCenter.getInstance(currentAccount).onAnimationFinish(transitionIndex); - } - } - - @Keep - public float getAnimationProgress() { - return animationProgress; - } - - @Keep - public void setAnimationProgress(float progress) { - animationProgress = progress; - - listView.setAlpha(progress); - - listView.setTranslationX(AndroidUtilities.dp(48) - AndroidUtilities.dp(48) * progress); - - int color = Theme.getColor(Theme.key_avatar_backgroundActionBarBlue); - int actionBarColor = Theme.getColor(Theme.key_actionBarDefault); - int r = Color.red(actionBarColor); - int g = Color.green(actionBarColor); - int b = Color.blue(actionBarColor); - - int rD = (int) ((Color.red(color) - r) * progress); - int gD = (int) ((Color.green(color) - g) * progress); - int bD = (int) ((Color.blue(color) - b) * progress); - topView.setBackgroundColor(Color.rgb(r + rD, g + gD, b + bD)); - - color = Theme.getColor(Theme.key_avatar_actionBarIconBlue); - int iconColor = Theme.getColor(Theme.key_actionBarDefaultIcon); - r = Color.red(iconColor); - g = Color.green(iconColor); - b = Color.blue(iconColor); - - rD = (int) ((Color.red(color) - r) * progress); - gD = (int) ((Color.green(color) - g) * progress); - bD = (int) ((Color.blue(color) - b) * progress); - actionBar.setItemsColor(Color.rgb(r + rD, g + gD, b + bD), false); - - titleTextView.setAlpha(1.0f - progress); - nameTextView.setAlpha(progress); - onlineTextView.setAlpha(progress); - idTextView.setAlpha(progress); - - extraHeight = (int) (initialAnimationExtraHeight * progress); - avatarContainer.setAlpha(progress); - - needLayout(); - } - - @Override - protected AnimatorSet onCustomTransitionAnimation(final boolean isOpen, final Runnable callback) { - if (playProfileAnimation && allowProfileAnimation) { - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.setDuration(180); - listView.setLayerType(View.LAYER_TYPE_HARDWARE, null); - ActionBarMenu menu = actionBar.createMenu(); - if (isOpen) { - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) onlineTextView.getLayoutParams(); - layoutParams.rightMargin = (int) (-21 * AndroidUtilities.density + AndroidUtilities.dp(8)); - onlineTextView.setLayoutParams(layoutParams); - idTextView.setLayoutParams(layoutParams); - - int width = (int) Math.ceil(AndroidUtilities.displaySize.x - AndroidUtilities.dp(118 + 8) + 21 * AndroidUtilities.density); - float width2 = nameTextView.getPaint().measureText(nameTextView.getText().toString()) * 1.12f; - layoutParams = (FrameLayout.LayoutParams) nameTextView.getLayoutParams(); - if (width < width2) { - layoutParams.width = (int) Math.ceil(width / 1.12f); - } else { - layoutParams.width = LayoutHelper.WRAP_CONTENT; - } - nameTextView.setLayoutParams(layoutParams); - - initialAnimationExtraHeight = AndroidUtilities.dp(88); - setAnimationProgress(0); - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimator.ofFloat(this, "animationProgress", 0.0f, 1.0f)); - if (writeButton != null) { - writeButton.setScaleX(0.2f); - writeButton.setScaleY(0.2f); - writeButton.setAlpha(0.0f); - animators.add(ObjectAnimator.ofFloat(writeButton, View.SCALE_X, 1.0f)); - animators.add(ObjectAnimator.ofFloat(writeButton, View.SCALE_Y, 1.0f)); - animators.add(ObjectAnimator.ofFloat(writeButton, View.ALPHA, 1.0f)); - } - animators.add(ObjectAnimator.ofFloat(onlineTextView, View.ALPHA, 0.0f, 1.0f)); - animators.add(ObjectAnimator.ofFloat(idTextView, View.ALPHA, 0.0f, 1.0f)); - animators.add(ObjectAnimator.ofFloat(nameTextView, View.ALPHA, 0.0f, 1.0f)); - searchItem.setTranslationX(AndroidUtilities.dp(48)); - otherItem.setTranslationX(AndroidUtilities.dp(48)); - animators.add(ObjectAnimator.ofFloat(searchItem, View.TRANSLATION_X, 0)); - animators.add(ObjectAnimator.ofFloat(otherItem, View.TRANSLATION_X, 0)); - animatorSet.playTogether(animators); - } else { - initialAnimationExtraHeight = extraHeight; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimator.ofFloat(this, "animationProgress", 1.0f, 0.0f)); - if (writeButton != null) { - animators.add(ObjectAnimator.ofFloat(writeButton, View.SCALE_X, 0.2f)); - animators.add(ObjectAnimator.ofFloat(writeButton, View.SCALE_Y, 0.2f)); - animators.add(ObjectAnimator.ofFloat(writeButton, View.ALPHA, 0.0f)); - } - animators.add(ObjectAnimator.ofFloat(onlineTextView, View.ALPHA, 0.0f)); - animators.add(ObjectAnimator.ofFloat(idTextView, View.ALPHA, 0.0f)); - animators.add(ObjectAnimator.ofFloat(nameTextView, View.ALPHA, 0.0f)); - animators.add(ObjectAnimator.ofFloat(searchItem, View.TRANSLATION_X, AndroidUtilities.dp(48))); - animators.add(ObjectAnimator.ofFloat(otherItem, View.TRANSLATION_X, AndroidUtilities.dp(48))); - animatorSet.playTogether(animators); - } - animatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - listView.setLayerType(View.LAYER_TYPE_NONE, null); - callback.run(); - } - }); - animatorSet.setInterpolator(CubicBezierInterpolator.EASE_OUT); - - AndroidUtilities.runOnUIThread(animatorSet::start, 50); - return animatorSet; - } - return null; - } - - public void setPlayProfileAnimation(boolean value) { - SharedPreferences preferences = MessagesController.getGlobalMainSettings(); - if (!AndroidUtilities.isTablet() && preferences.getBoolean("view_animations", true)) { - playProfileAnimation = value; - } - } - - private void checkListViewScroll() { - if (listView.getVisibility() != View.VISIBLE || listView.getChildCount() <= 0 || openAnimationInProgress || writeButton.getVisibility() != View.VISIBLE) { - return; - } - - View child = listView.getChildAt(0); - RecyclerListView.Holder holder = (RecyclerListView.Holder) listView.findContainingViewHolder(child); - int top = child.getTop(); - int newOffset = 0; - if (top >= 0 && holder != null && holder.getAdapterPosition() == 0) { - newOffset = top; - } - if (extraHeight != newOffset) { - extraHeight = newOffset; - topView.invalidate(); - needLayout(); - } - } - - private void needLayout() { - FrameLayout.LayoutParams layoutParams; - int newTop = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight(); - if (listView != null && !openAnimationInProgress) { - layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); - if (layoutParams.topMargin != newTop) { - layoutParams.topMargin = newTop; - listView.setLayoutParams(layoutParams); - searchListView.setLayoutParams(layoutParams); - } - } - - if (avatarContainer != null) { - float diff = extraHeight / (float) AndroidUtilities.dp(88); - listView.setTopGlowOffset(extraHeight); - - if (writeButton != null) { - writeButton.setTranslationY((actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight + searchTransitionOffset - AndroidUtilities.dp(29.5f)); - - if (!openAnimationInProgress) { - final boolean setVisible = diff > 0.2f && !searchMode; - boolean currentVisible = writeButton.getTag() == null; - if (setVisible != currentVisible) { - if (setVisible) { - writeButton.setTag(null); - } else { - writeButton.setTag(0); - } - if (writeButtonAnimation != null) { - AnimatorSet old = writeButtonAnimation; - writeButtonAnimation = null; - old.cancel(); - } - writeButtonAnimation = new AnimatorSet(); - if (setVisible) { - writeButtonAnimation.setInterpolator(new DecelerateInterpolator()); - writeButtonAnimation.playTogether( - ObjectAnimator.ofFloat(writeButton, View.SCALE_X, 1.0f), - ObjectAnimator.ofFloat(writeButton, View.SCALE_Y, 1.0f), - ObjectAnimator.ofFloat(writeButton, View.ALPHA, 1.0f) - ); - } else { - writeButtonAnimation.setInterpolator(new AccelerateInterpolator()); - writeButtonAnimation.playTogether( - ObjectAnimator.ofFloat(writeButton, View.SCALE_X, 0.2f), - ObjectAnimator.ofFloat(writeButton, View.SCALE_Y, 0.2f), - ObjectAnimator.ofFloat(writeButton, View.ALPHA, 0.0f) - ); - } - writeButtonAnimation.setDuration(150); - writeButtonAnimation.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (writeButtonAnimation != null && writeButtonAnimation.equals(animation)) { - writeButtonAnimation = null; - } - } - }); - writeButtonAnimation.start(); - } - } - } - - float avatarY = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() / 2.0f * (1.0f + diff) - 21 * AndroidUtilities.density + 27 * AndroidUtilities.density * diff; - avatarContainer.setScaleX((42 + 18 * diff) / 42.0f); - avatarContainer.setScaleY((42 + 18 * diff) / 42.0f); - avatarContainer.setTranslationX(-AndroidUtilities.dp(47) * diff); - avatarContainer.setTranslationY((float) Math.ceil(avatarY)); - - if (nameTextView != null) { - nameTextView.setTranslationX(-21 * AndroidUtilities.density * diff); - onlineTextView.setTranslationX(-21 * AndroidUtilities.density * diff); - - nameTextView.setTranslationY((float) Math.floor(avatarY) - (float) Math.ceil(AndroidUtilities.density) + (float) Math.floor(7 * AndroidUtilities.density * diff)); - onlineTextView.setTranslationY((float) Math.floor(avatarY) + AndroidUtilities.dp(22) + (float) Math.floor(11 * AndroidUtilities.density) * diff); - - float scale = 1.0f + 0.12f * diff; - nameTextView.setScaleX(scale); - nameTextView.setScaleY(scale); - idTextView.setTranslationX(-21 * AndroidUtilities.density * diff); - idTextView.setTranslationY((float) Math.floor(avatarY) + AndroidUtilities.dp(32) + (float) Math.floor(22 * AndroidUtilities.density) * diff); - if (diff > 0.85 && !searchMode && NekoConfig.showIdAndDc) { - idTextView.setVisibility(View.VISIBLE); - } else { - idTextView.setVisibility(View.GONE); - } - if (!openAnimationInProgress) { - int viewWidth; - if (AndroidUtilities.isTablet()) { - viewWidth = AndroidUtilities.dp(490); - } else { - viewWidth = AndroidUtilities.displaySize.x; - } - int buttonsWidth = AndroidUtilities.dp(118 + 8 + 40 + 48); - int minWidth = viewWidth - buttonsWidth; - - int width = (int) (viewWidth - buttonsWidth * Math.max(0.0f, 1.0f - (diff != 1.0f ? diff * 0.15f / (1.0f - diff) : 1.0f)) - nameTextView.getTranslationX()); - float width2 = nameTextView.getPaint().measureText(nameTextView.getText().toString()) * scale; - layoutParams = (FrameLayout.LayoutParams) nameTextView.getLayoutParams(); - if (width < width2) { - layoutParams.width = Math.max(minWidth, (int) Math.ceil((width - AndroidUtilities.dp(24)) / (scale + (1.12f - scale) * 7.0f))); - } else { - layoutParams.width = (int) Math.ceil(width2); - } - layoutParams.width = (int) Math.min((viewWidth - nameTextView.getX()) / scale - AndroidUtilities.dp(8), layoutParams.width); - nameTextView.setLayoutParams(layoutParams); - - width2 = onlineTextView.getPaint().measureText(onlineTextView.getText().toString()); - layoutParams = (FrameLayout.LayoutParams) onlineTextView.getLayoutParams(); - layoutParams.rightMargin = (int) Math.ceil(onlineTextView.getTranslationX() + AndroidUtilities.dp(8) + AndroidUtilities.dp(40) * (1.0f - diff)); - if (width < width2) { - layoutParams.width = (int) Math.ceil(width); - } else { - layoutParams.width = LayoutHelper.WRAP_CONTENT; - } - onlineTextView.setLayoutParams(layoutParams); - - width2 = idTextView.getPaint().measureText(idTextView.getText().toString()); - layoutParams = (FrameLayout.LayoutParams) idTextView.getLayoutParams(); - layoutParams.rightMargin = (int) Math.ceil(idTextView.getTranslationX() + AndroidUtilities.dp(8) + AndroidUtilities.dp(40) * (1.0f - diff)); - if (width < width2) { - layoutParams.width = (int) Math.ceil(width); - } else { - layoutParams.width = LayoutHelper.WRAP_CONTENT; - } - idTextView.setLayoutParams(layoutParams); - } - } - } - } - - private void fixLayout() { - if (fragmentView == null) { - return; - } - fragmentView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - if (fragmentView != null) { - checkListViewScroll(); - needLayout(); - fragmentView.getViewTreeObserver().removeOnPreDrawListener(this); - } - return true; - } - }); - } - - private void updateUserData() { - TLRPC.User user = MessagesController.getInstance(currentAccount).getUser(UserConfig.getInstance(currentAccount).getClientUserId()); - if (user == null) { - return; - } - TLRPC.FileLocation photoBig = null; - if (user.photo != null) { - photoBig = user.photo.photo_big; - } - avatarDrawable = new AvatarDrawable(user, true); - - avatarDrawable.setColor(Theme.getColor(Theme.key_avatar_backgroundInProfileBlue)); - if (avatarImage != null) { - avatarImage.setImage(ImageLocation.getForUser(user, false), "50_50", avatarDrawable, user); - avatarImage.getImageReceiver().setVisible(!PhotoViewer.isShowingImage(photoBig), false); - - nameTextView.setText(UserObject.getUserName(user)); - onlineTextView.setText(LocaleController.getString("Online", R.string.Online)); - - avatarImage.getImageReceiver().setVisible(!PhotoViewer.isShowingImage(photoBig), false); - } - if (user.photo != null && user.photo.dc_id != 0) { - idTextView.setText("ID: " + user.id + ", DC: " + user.photo.dc_id); - } else { - idTextView.setText("ID: " + user.id + ", DC: " + getMessagesController().thisDc); - } - int finalId = user.id; - idTextView.setOnLongClickListener(v -> { - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setItems(new CharSequence[]{LocaleController.getString("Copy", R.string.Copy)}, (dialogInterface, i) -> { - if (i == 0) { - try { - AndroidUtilities.addToClipboard(String.valueOf(finalId)); - Toast.makeText(getParentActivity(), LocaleController.getString("TextCopied", R.string.TextCopied), Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - FileLog.e(e); - } - } - }); - showDialog(builder.create()); - return false; - }); - } - - private void sendLogs() { - - File path = new File(EnvUtil.getTelegramPath(), "logs"); - - File logcatFile = new File(path, "NekoX-" + System.currentTimeMillis() + ".log"); - - FileUtil.delete(logcatFile); - - try { - - Process process = RuntimeUtil.exec("logcat", "-d"); - - IoUtil.copy(process, logcatFile); - - RuntimeUtil.exec("logcat", "-c"); - - ShareUtil.shareFile(getParentActivity(), logcatFile); - - } catch (Exception e) { - - AlertUtil.showToast(e); - - } - - } - - private class SearchAdapter extends RecyclerListView.SelectionAdapter { - - private class SearchResult { - - private String searchTitle; - private Runnable openRunnable; - private String rowName; - private String[] path; - private int iconResId; - private int guid; - private int num; - - public SearchResult(int g, String search, int icon, Runnable open) { - this(g, search, null, null, null, icon, open); - } - - public SearchResult(int g, String search, String pathArg1, int icon, Runnable open) { - this(g, search, null, pathArg1, null, icon, open); - } - - public SearchResult(int g, String search, String row, String pathArg1, int icon, Runnable open) { - this(g, search, row, pathArg1, null, icon, open); - } - - public SearchResult(int g, String search, String row, String pathArg1, String pathArg2, int icon, Runnable open) { - guid = g; - searchTitle = search; - rowName = row; - openRunnable = open; - iconResId = icon; - if (pathArg1 != null && pathArg2 != null) { - path = new String[]{pathArg1, pathArg2}; - } else if (pathArg1 != null) { - path = new String[]{pathArg1}; - } - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof SearchResult)) { - return false; - } - SearchResult result = (SearchResult) obj; - return guid == result.guid; - } - - @Override - public String toString() { - SerializedData data = new SerializedData(); - data.writeInt32(num); - data.writeInt32(1); - data.writeInt32(guid); - return Utilities.bytesToHex(data.toByteArray()); - } - - private void open() { - openRunnable.run(); - if (rowName != null) { - - BaseFragment openingFragment = parentLayout.fragmentsStack.get(parentLayout.fragmentsStack.size() - 1); - try { - Field listViewField = openingFragment.getClass().getDeclaredField("listView"); - listViewField.setAccessible(true); - RecyclerListView.IntReturnCallback callback = () -> { - int position = -1; - try { - Field rowField = openingFragment.getClass().getDeclaredField(rowName); - Field linearLayoutField = openingFragment.getClass().getDeclaredField("layoutManager"); - rowField.setAccessible(true); - linearLayoutField.setAccessible(true); - LinearLayoutManager layoutManager = (LinearLayoutManager) linearLayoutField.get(openingFragment); - position = rowField.getInt(openingFragment); - layoutManager.scrollToPositionWithOffset(position, 0); - rowField.setAccessible(false); - linearLayoutField.setAccessible(false); - return position; - } catch (Throwable ignore) { - - } - return position; - }; - RecyclerListView listView = (RecyclerListView) listViewField.get(openingFragment); - listView.highlightRow(callback); - listViewField.setAccessible(false); - } catch (Throwable ignore) { - - } - } - } - } - - private class FaqSearchResult { - - private String title; - private String[] path; - private String url; - private int num; - - public FaqSearchResult(String t, String[] p, String u) { - title = t; - path = p; - url = u; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof FaqSearchResult)) { - return false; - } - FaqSearchResult result = (FaqSearchResult) obj; - return title.equals(result.title); - } - - @Override - public String toString() { - SerializedData data = new SerializedData(); - data.writeInt32(num); - data.writeInt32(0); - data.writeString(title); - data.writeInt32(path != null ? path.length : 0); - if (path != null) { - for (int a = 0; a < path.length; a++) { - data.writeString(path[a]); - } - } - data.writeString(url); - return Utilities.bytesToHex(data.toByteArray()); - } - } - - private SearchResult[] searchArray = new SearchResult[]{ - new SearchResult(500, LocaleController.getString("EditName", R.string.EditName), 0, () -> presentFragment(new ChangeNameActivity())), - new SearchResult(501, LocaleController.getString("ChangePhoneNumber", R.string.ChangePhoneNumber), 0, () -> presentFragment(new ActionIntroActivity(ActionIntroActivity.ACTION_TYPE_CHANGE_PHONE_NUMBER))), - new SearchResult(502, LocaleController.getString("AddAnotherAccount", R.string.AddAnotherAccount), 0, () -> { - int freeAccount = -1; - for (int a = 0; a < UserConfig.MAX_ACCOUNT_COUNT; a++) { - if (!UserConfig.getInstance(a).isClientActivated()) { - freeAccount = a; - break; - } - } - if (freeAccount >= 0) { - presentFragment(new LoginActivity(freeAccount)); - } - }), - new SearchResult(503, LocaleController.getString("UserBio", R.string.UserBio), 0, () -> { - if (userInfo != null) { - presentFragment(new ChangeBioActivity()); - } - }), - - new SearchResult(1, LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsSettingsActivity())), - new SearchResult(2, LocaleController.getString("NotificationsPrivateChats", R.string.NotificationsPrivateChats), LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsCustomSettingsActivity(NotificationsController.TYPE_PRIVATE, new ArrayList<>(), true))), - new SearchResult(3, LocaleController.getString("NotificationsGroups", R.string.NotificationsGroups), LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsCustomSettingsActivity(NotificationsController.TYPE_GROUP, new ArrayList<>(), true))), - new SearchResult(4, LocaleController.getString("NotificationsChannels", R.string.NotificationsChannels), LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsCustomSettingsActivity(NotificationsController.TYPE_CHANNEL, new ArrayList<>(), true))), - new SearchResult(5, LocaleController.getString("VoipNotificationSettings", R.string.VoipNotificationSettings), "callsSectionRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsSettingsActivity())), - new SearchResult(6, LocaleController.getString("BadgeNumber", R.string.BadgeNumber), "badgeNumberSection", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsSettingsActivity())), - new SearchResult(7, LocaleController.getString("InAppNotifications", R.string.InAppNotifications), "inappSectionRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsSettingsActivity())), - new SearchResult(8, LocaleController.getString("ContactJoined", R.string.ContactJoined), "contactJoinedRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsSettingsActivity())), - new SearchResult(9, LocaleController.getString("PinnedMessages", R.string.PinnedMessages), "pinnedMessageRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsSettingsActivity())), - new SearchResult(10, LocaleController.getString("ResetAllNotifications", R.string.ResetAllNotifications), "resetNotificationsRow", LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, () -> presentFragment(new NotificationsSettingsActivity())), - - new SearchResult(100, LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(101, LocaleController.getString("BlockedUsers", R.string.BlockedUsers), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacyUsersActivity())), - new SearchResult(105, LocaleController.getString("PrivacyPhone", R.string.PrivacyPhone), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_PHONE, true))), - new SearchResult(102, LocaleController.getString("PrivacyLastSeen", R.string.PrivacyLastSeen), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_LASTSEEN, true))), - new SearchResult(103, LocaleController.getString("PrivacyProfilePhoto", R.string.PrivacyProfilePhoto), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_PHOTO, true))), - new SearchResult(104, LocaleController.getString("PrivacyForwards", R.string.PrivacyForwards), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_FORWARDS, true))), - new SearchResult(105, LocaleController.getString("PrivacyP2P", R.string.PrivacyP2P), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_P2P, true))), - new SearchResult(106, LocaleController.getString("Calls", R.string.Calls), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_CALLS, true))), - new SearchResult(107, LocaleController.getString("GroupsAndChannels", R.string.GroupsAndChannels), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacyControlActivity(ContactsController.PRIVACY_RULES_TYPE_INVITE, true))), - new SearchResult(108, LocaleController.getString("Passcode", R.string.Passcode), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PasscodeActivity(SharedConfig.passcodeHash.length() > 0 ? 2 : 0))), - new SearchResult(109, LocaleController.getString("TwoStepVerification", R.string.TwoStepVerification), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new TwoStepVerificationActivity())), - new SearchResult(110, LocaleController.getString("SessionsTitle", R.string.SessionsTitle), R.drawable.baseline_security_24, () -> presentFragment(new SessionsActivity(0))), - new SearchResult(111, LocaleController.getString("PrivacyDeleteCloudDrafts", R.string.PrivacyDeleteCloudDrafts), "clearDraftsRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(112, LocaleController.getString("DeleteAccountIfAwayFor2", R.string.DeleteAccountIfAwayFor2), "deleteAccountRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(113, LocaleController.getString("PrivacyPaymentsClear", R.string.PrivacyPaymentsClear), "paymentsClearRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(114, LocaleController.getString("WebSessionsTitle", R.string.WebSessionsTitle), LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new SessionsActivity(1))), - new SearchResult(115, LocaleController.getString("SyncContactsDelete", R.string.SyncContactsDelete), "contactsDeleteRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(116, LocaleController.getString("SyncContacts", R.string.SyncContacts), "contactsSyncRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(117, LocaleController.getString("SuggestContacts", R.string.SuggestContacts), "contactsSuggestRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(118, LocaleController.getString("MapPreviewProvider", R.string.MapPreviewProvider), "secretMapRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(119, LocaleController.getString("SecretWebPage", R.string.SecretWebPage), "secretWebpageRow", LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_security_24, () -> presentFragment(new PrivacySettingsActivity())), - new SearchResult(120, LocaleController.getString("Devices", R.string.Devices), R.drawable.baseline_security_24, () -> presentFragment(new SessionsActivity(0))), - - new SearchResult(200, LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(201, LocaleController.getString("DataUsage", R.string.DataUsage), "usageSectionRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(202, LocaleController.getString("StorageUsage", R.string.StorageUsage), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new CacheControlActivity())), - new SearchResult(203, LocaleController.getString("KeepMedia", R.string.KeepMedia), "keepMediaRow", LocaleController.getString("DataSettings", R.string.DataSettings), LocaleController.getString("StorageUsage", R.string.StorageUsage), R.drawable.baseline_data_usage_24, () -> presentFragment(new CacheControlActivity())), - new SearchResult(204, LocaleController.getString("ClearMediaCache", R.string.ClearMediaCache), "cacheRow", LocaleController.getString("DataSettings", R.string.DataSettings), LocaleController.getString("StorageUsage", R.string.StorageUsage), R.drawable.baseline_data_usage_24, () -> presentFragment(new CacheControlActivity())), - new SearchResult(205, LocaleController.getString("LocalDatabase", R.string.LocalDatabase), "databaseRow", LocaleController.getString("DataSettings", R.string.DataSettings), LocaleController.getString("StorageUsage", R.string.StorageUsage), R.drawable.baseline_data_usage_24, () -> presentFragment(new CacheControlActivity())), - new SearchResult(206, LocaleController.getString("NetworkUsage", R.string.NetworkUsage), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataUsageActivity())), - new SearchResult(207, LocaleController.getString("AutomaticMediaDownload", R.string.AutomaticMediaDownload), "mediaDownloadSectionRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(208, LocaleController.getString("WhenUsingMobileData", R.string.WhenUsingMobileData), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataAutoDownloadActivity(0))), - new SearchResult(209, LocaleController.getString("WhenConnectedOnWiFi", R.string.WhenConnectedOnWiFi), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataAutoDownloadActivity(1))), - new SearchResult(210, LocaleController.getString("WhenRoaming", R.string.WhenRoaming), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataAutoDownloadActivity(2))), - new SearchResult(211, LocaleController.getString("ResetAutomaticMediaDownload", R.string.ResetAutomaticMediaDownload), "resetDownloadRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(212, LocaleController.getString("AutoplayMedia", R.string.AutoplayMedia), "autoplayHeaderRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(213, LocaleController.getString("AutoplayGIF", R.string.AutoplayGIF), "autoplayGifsRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(214, LocaleController.getString("AutoplayVideo", R.string.AutoplayVideo), "autoplayVideoRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(215, LocaleController.getString("Streaming", R.string.Streaming), "streamSectionRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(216, LocaleController.getString("EnableStreaming", R.string.EnableStreaming), "enableStreamRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(217, LocaleController.getString("Calls", R.string.Calls), "callsSectionRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(218, LocaleController.getString("VoipUseLessData", R.string.VoipUseLessData), "useLessDataForCallsRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(219, LocaleController.getString("VoipQuickReplies", R.string.VoipQuickReplies), "quickRepliesRow", LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new DataSettingsActivity())), - new SearchResult(220, LocaleController.getString("ProxySettings", R.string.ProxySettings), LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new ProxyListActivity())), - new SearchResult(221, LocaleController.getString("UseProxyForCalls", R.string.UseProxyForCalls), "callsRow", LocaleController.getString("DataSettings", R.string.DataSettings), LocaleController.getString("ProxySettings", R.string.ProxySettings), R.drawable.baseline_data_usage_24, () -> presentFragment(new ProxyListActivity())), - - new SearchResult(300, LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(301, LocaleController.getString("TextSizeHeader", R.string.TextSizeHeader), "textSizeHeaderRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(302, LocaleController.getString("ChatBackground", R.string.ChatBackground), LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new WallpapersListActivity(WallpapersListActivity.TYPE_ALL))), - new SearchResult(303, LocaleController.getString("SetColor", R.string.SetColor), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("ChatBackground", R.string.ChatBackground), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new WallpapersListActivity(WallpapersListActivity.TYPE_COLOR))), - new SearchResult(304, LocaleController.getString("ResetChatBackgrounds", R.string.ResetChatBackgrounds), "resetRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("ChatBackground", R.string.ChatBackground), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new WallpapersListActivity(WallpapersListActivity.TYPE_ALL))), - new SearchResult(305, LocaleController.getString("AutoNightTheme", R.string.AutoNightTheme), LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_NIGHT))), - new SearchResult(306, LocaleController.getString("ColorTheme", R.string.ColorTheme), "themeHeaderRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(307, LocaleController.getString("ChromeCustomTabs", R.string.ChromeCustomTabs), "customTabsRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(308, LocaleController.getString("DirectShare", R.string.DirectShare), "directShareRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(309, LocaleController.getString("EnableAnimations", R.string.EnableAnimations), "enableAnimationsRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(310, LocaleController.getString("RaiseToSpeak", R.string.RaiseToSpeak), "raiseToSpeakRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(311, LocaleController.getString("SendByEnter", R.string.SendByEnter), "sendByEnterRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(312, LocaleController.getString("SaveToGallerySettings", R.string.SaveToGallerySettings), "saveToGalleryRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(312, LocaleController.getString("DistanceUnits", R.string.DistanceUnits), "distanceRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC))), - new SearchResult(313, LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new StickersActivity(MediaDataController.TYPE_IMAGE))), - new SearchResult(314, LocaleController.getString("SuggestStickers", R.string.SuggestStickers), "suggestRow", LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new StickersActivity(MediaDataController.TYPE_IMAGE))), - new SearchResult(315, LocaleController.getString("FeaturedStickers", R.string.FeaturedStickers), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new FeaturedStickersActivity())), - new SearchResult(316, LocaleController.getString("Masks", R.string.Masks), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new StickersActivity(MediaDataController.TYPE_MASK))), - new SearchResult(317, LocaleController.getString("ArchivedStickers", R.string.ArchivedStickers), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ArchivedStickersActivity(MediaDataController.TYPE_IMAGE))), - new SearchResult(317, LocaleController.getString("ArchivedMasks", R.string.ArchivedMasks), null, LocaleController.getString("ChatSettings", R.string.ChatSettings), LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.baseline_chat_bubble_24, () -> presentFragment(new ArchivedStickersActivity(MediaDataController.TYPE_MASK))), - - new SearchResult(400, LocaleController.getString("Language", R.string.Language), R.drawable.baseline_language_24, () -> presentFragment(new LanguageSelectActivity())), - - new SearchResult(402, LocaleController.getString("AskAQuestion", R.string.AskAQuestion), LocaleController.getString("SettingsHelp", R.string.SettingsHelp), R.drawable.baseline_help_24, () -> showDialog(AlertsCreator.createSupportAlert(SettingsActivity.this))), - new SearchResult(403, LocaleController.getString("TelegramFAQ", R.string.TelegramFAQ), LocaleController.getString("SettingsHelp", R.string.SettingsHelp), R.drawable.baseline_help_24, () -> Browser.openUrl(getParentActivity(), LocaleController.getString("TelegramFaqUrl", R.string.TelegramFaqUrl))), - new SearchResult(404, LocaleController.getString("PrivacyPolicy", R.string.PrivacyPolicy), LocaleController.getString("SettingsHelp", R.string.SettingsHelp), R.drawable.baseline_help_24, () -> Browser.openUrl(getParentActivity(), LocaleController.getString("PrivacyPolicyUrl", R.string.PrivacyPolicyUrl))), - }; - private ArrayList faqSearchArray = new ArrayList<>(); - - private Context mContext; - private ArrayList resultNames = new ArrayList<>(); - private ArrayList searchResults = new ArrayList<>(); - private ArrayList faqSearchResults = new ArrayList<>(); - private ArrayList recentSearches = new ArrayList<>(); - private boolean searchWas; - private Runnable searchRunnable; - private String lastSearchString; - private TLRPC.WebPage faqWebPage; - private boolean loadingFaqPage; - - public SearchAdapter(Context context) { - mContext = context; - - HashMap resultHashMap = new HashMap<>(); - for (int a = 0; a < searchArray.length; a++) { - resultHashMap.put(searchArray[a].guid, searchArray[a]); - } - Set set = MessagesController.getGlobalMainSettings().getStringSet("settingsSearchRecent2", null); - if (set != null) { - for (String value : set) { - try { - SerializedData data = new SerializedData(Utilities.hexToBytes(value)); - int num = data.readInt32(false); - int type = data.readInt32(false); - if (type == 0) { - String title = data.readString(false); - int count = data.readInt32(false); - String[] path = null; - if (count > 0) { - path = new String[count]; - for (int a = 0; a < count; a++) { - path[a] = data.readString(false); - } - } - String url = data.readString(false); - FaqSearchResult result = new FaqSearchResult(title, path, url); - result.num = num; - recentSearches.add(result); - } else if (type == 1) { - SearchResult result = resultHashMap.get(data.readInt32(false)); - if (result != null) { - result.num = num; - recentSearches.add(result); - } - } - } catch (Exception ignore) { - - } - } - } - Collections.sort(recentSearches, (o1, o2) -> { - int n1 = getNum(o1); - int n2 = getNum(o2); - if (n1 < n2) { - return -1; - } else if (n1 > n2) { - return 1; - } - return 0; - }); - } - - private void loadFaqWebPage() { - if (faqWebPage != null || loadingFaqPage) { - return; - } - loadingFaqPage = true; - final TLRPC.TL_messages_getWebPage req2 = new TLRPC.TL_messages_getWebPage(); - req2.url = NekoXConfig.FAQ_URL; - req2.hash = 0; - ConnectionsManager.getInstance(currentAccount).sendRequest(req2, (response2, error2) -> { - if (response2 instanceof TLRPC.WebPage) { - TLRPC.WebPage page = (TLRPC.WebPage) response2; - if (page.cached_page != null) { - for (int a = 0, N = page.cached_page.blocks.size(); a < N; a++) { - TLRPC.PageBlock block = page.cached_page.blocks.get(a); - if (block instanceof TLRPC.TL_pageBlockList) { - String paragraph = null; - if (a != 0) { - TLRPC.PageBlock prevBlock = page.cached_page.blocks.get(a - 1); - if (prevBlock instanceof TLRPC.TL_pageBlockParagraph) { - TLRPC.TL_pageBlockParagraph pageBlockParagraph = (TLRPC.TL_pageBlockParagraph) prevBlock; - paragraph = ArticleViewer.getPlainText(pageBlockParagraph.text).toString(); - } - } - TLRPC.TL_pageBlockList list = (TLRPC.TL_pageBlockList) block; - for (int b = 0, N2 = list.items.size(); b < N2; b++) { - TLRPC.PageListItem item = list.items.get(b); - if (item instanceof TLRPC.TL_pageListItemText) { - TLRPC.TL_pageListItemText itemText = (TLRPC.TL_pageListItemText) item; - String url = ArticleViewer.getUrl(itemText.text); - String text = ArticleViewer.getPlainText(itemText.text).toString(); - if (TextUtils.isEmpty(url) || TextUtils.isEmpty(text)) { - continue; - } - String[] path; - if (paragraph != null) { - path = new String[]{LocaleController.getString("SettingsSearchFaq", R.string.SettingsSearchFaq), paragraph}; - } else { - path = new String[]{LocaleController.getString("SettingsSearchFaq", R.string.SettingsSearchFaq)}; - } - faqSearchArray.add(new FaqSearchResult(text, path, url)); - } - } - } else if (block instanceof TLRPC.TL_pageBlockAnchor) { - break; - } - } - faqWebPage = page; - } - } - loadingFaqPage = false; - }); - } - - @Override - public int getItemCount() { - if (searchWas) { - return searchResults.size() + (faqSearchResults.isEmpty() ? 0 : 1 + faqSearchResults.size()); - } - return (recentSearches.isEmpty() ? 0 : recentSearches.size() + 1); - } - - @Override - public boolean isEnabled(RecyclerView.ViewHolder holder) { - return holder.getItemViewType() == 0; - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - switch (holder.getItemViewType()) { - case 0: { - SettingsSearchCell searchCell = (SettingsSearchCell) holder.itemView; - if (searchWas) { - if (position < searchResults.size()) { - SearchResult result = searchResults.get(position); - SearchResult prevResult = position > 0 ? searchResults.get(position - 1) : null; - int icon; - if (prevResult != null && prevResult.iconResId == result.iconResId) { - icon = 0; - } else { - icon = result.iconResId; - } - searchCell.setTextAndValueAndIcon(resultNames.get(position), result.path, icon, position < searchResults.size() - 1); - } else { - position -= searchResults.size() + 1; - FaqSearchResult result = faqSearchResults.get(position); - searchCell.setTextAndValue(resultNames.get(position + searchResults.size()), result.path, true, position < searchResults.size() - 1); - } - } else { - position--; - Object object = recentSearches.get(position); - if (object instanceof SearchResult) { - SearchResult result = (SearchResult) object; - searchCell.setTextAndValue(result.searchTitle, result.path, false, position < recentSearches.size() - 1); - } else if (object instanceof FaqSearchResult) { - FaqSearchResult result = (FaqSearchResult) object; - searchCell.setTextAndValue(result.title, result.path, true, position < recentSearches.size() - 1); - } - } - break; - } - case 1: { - GraySectionCell sectionCell = (GraySectionCell) holder.itemView; - sectionCell.setText(LocaleController.getString("SettingsFaqSearchTitle", R.string.SettingsFaqSearchTitle)); - break; - } - case 2: { - HeaderCell headerCell = (HeaderCell) holder.itemView; - headerCell.setText(LocaleController.getString("SettingsRecent", R.string.SettingsRecent)); - break; - } - } - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view; - switch (viewType) { - case 0: - view = new SettingsSearchCell(mContext); - break; - case 1: - view = new GraySectionCell(mContext); - break; - case 2: - default: - view = new HeaderCell(mContext, 16); - break; - } - view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); - return new RecyclerListView.Holder(view); - } - - @Override - public int getItemViewType(int position) { - if (searchWas) { - if (position < searchResults.size()) { - return 0; - } else if (position == searchResults.size()) { - return 1; - } - return 0; - } else { - if (position == 0) { - return 2; - } - return 0; - } - } - - public void addRecent(Object object) { - int index = recentSearches.indexOf(object); - if (index >= 0) { - recentSearches.remove(index); - } - recentSearches.add(0, object); - if (!searchWas) { - notifyDataSetChanged(); - } - if (recentSearches.size() > 20) { - recentSearches.remove(recentSearches.size() - 1); - } - LinkedHashSet toSave = new LinkedHashSet<>(); - for (int a = 0, N = recentSearches.size(); a < N; a++) { - Object o = recentSearches.get(a); - if (o instanceof SearchResult) { - ((SearchResult) o).num = a; - } else if (o instanceof FaqSearchResult) { - ((FaqSearchResult) o).num = a; - } - toSave.add(o.toString()); - } - MessagesController.getGlobalMainSettings().edit().putStringSet("settingsSearchRecent2", toSave).commit(); - } - - public void clearRecent() { - recentSearches.clear(); - MessagesController.getGlobalMainSettings().edit().remove("settingsSearchRecent2").commit(); - notifyDataSetChanged(); - } - - private int getNum(Object o) { - if (o instanceof SearchResult) { - return ((SearchResult) o).num; - } else if (o instanceof FaqSearchResult) { - return ((FaqSearchResult) o).num; - } - return 0; - } - - public void search(String text) { - lastSearchString = text; - if (searchRunnable != null) { - Utilities.searchQueue.cancelRunnable(searchRunnable); - searchRunnable = null; - } - if (TextUtils.isEmpty(text)) { - searchWas = false; - searchResults.clear(); - faqSearchResults.clear(); - resultNames.clear(); - emptyView.setTopImage(0); - emptyView.setText(LocaleController.getString("SettingsNoRecent", R.string.SettingsNoRecent)); - notifyDataSetChanged(); - return; - } - Utilities.searchQueue.postRunnable(searchRunnable = () -> { - ArrayList results = new ArrayList<>(); - ArrayList faqResults = new ArrayList<>(); - ArrayList names = new ArrayList<>(); - String[] searchArgs = text.split(" "); - String[] translitArgs = new String[searchArgs.length]; - for (int a = 0; a < searchArgs.length; a++) { - translitArgs[a] = LocaleController.getInstance().getTranslitString(searchArgs[a]); - if (translitArgs[a].equals(searchArgs[a])) { - translitArgs[a] = null; - } - } - - for (int a = 0; a < searchArray.length; a++) { - SearchResult result = searchArray[a]; - String title = " " + result.searchTitle.toLowerCase(); - SpannableStringBuilder stringBuilder = null; - for (int i = 0; i < searchArgs.length; i++) { - if (searchArgs[i].length() != 0) { - String searchString = searchArgs[i]; - int index = title.indexOf(" " + searchString); - if (index < 0 && translitArgs[i] != null) { - searchString = translitArgs[i]; - index = title.indexOf(" " + searchString); - } - if (index >= 0) { - if (stringBuilder == null) { - stringBuilder = new SpannableStringBuilder(result.searchTitle); - } - stringBuilder.setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4)), index, index + searchString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } else { - break; - } - } - if (stringBuilder != null && i == searchArgs.length - 1) { - if (result.guid == 502) { - int freeAccount = -1; - for (int b = 0; b < UserConfig.MAX_ACCOUNT_COUNT; b++) { - if (!UserConfig.getInstance(a).isClientActivated()) { - freeAccount = b; - break; - } - } - if (freeAccount < 0) { - continue; - } - } - results.add(result); - names.add(stringBuilder); - } - } - } - if (faqWebPage != null) { - for (int a = 0, N = faqSearchArray.size(); a < N; a++) { - FaqSearchResult result = faqSearchArray.get(a); - String title = " " + result.title.toLowerCase(); - SpannableStringBuilder stringBuilder = null; - for (int i = 0; i < searchArgs.length; i++) { - if (searchArgs[i].length() != 0) { - String searchString = searchArgs[i]; - int index = title.indexOf(" " + searchString); - if (index < 0 && translitArgs[i] != null) { - searchString = translitArgs[i]; - index = title.indexOf(" " + searchString); - } - if (index >= 0) { - if (stringBuilder == null) { - stringBuilder = new SpannableStringBuilder(result.title); - } - stringBuilder.setSpan(new ForegroundColorSpan(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText4)), index, index + searchString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } else { - break; - } - } - if (stringBuilder != null && i == searchArgs.length - 1) { - faqResults.add(result); - names.add(stringBuilder); - } - } - } - } - - AndroidUtilities.runOnUIThread(() -> { - if (!text.equals(lastSearchString)) { - return; - } - if (!searchWas) { - emptyView.setTopImage(R.drawable.settings_noresults); - emptyView.setText(LocaleController.getString("SettingsNoResults", R.string.SettingsNoResults)); - } - searchWas = true; - searchResults = results; - faqSearchResults = faqResults; - resultNames = names; - notifyDataSetChanged(); - }); - }, 300); - } - - public boolean isSearchWas() { - return searchWas; - } - } - - private class ListAdapter extends RecyclerListView.SelectionAdapter { - - private Context mContext; - - public ListAdapter(Context context) { - mContext = context; - } - - @Override - public int getItemCount() { - return rowCount; - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - switch (holder.getItemViewType()) { - case 2: { - TextCell textCell = (TextCell) holder.itemView; - if (position == languageRow) { - textCell.setTextAndIcon(LocaleController.getString("Language", R.string.Language), R.drawable.baseline_language_24, false); - } else if (position == notificationRow) { - textCell.setTextAndIcon(LocaleController.getString("NotificationsAndSounds", R.string.NotificationsAndSounds), R.drawable.baseline_notifications_24, true); - } else if (position == privacyRow) { - textCell.setTextAndIcon(LocaleController.getString("PrivacySettings", R.string.PrivacySettings), R.drawable.baseline_lock_24, true); - } else if (position == dataRow) { - textCell.setTextAndIcon(LocaleController.getString("DataSettings", R.string.DataSettings), R.drawable.baseline_data_usage_24, true); - } else if (position == chatRow) { - textCell.setTextAndIcon(LocaleController.getString("ChatSettings", R.string.ChatSettings), R.drawable.baseline_palette_24, true); - } else if (position == stickersRow) { - textCell.setTextAndIcon(LocaleController.getString("StickersAndMasks", R.string.StickersAndMasks), R.drawable.deproko_baseline_stickers_24, true); - } else if (position == nekoRow) { - textCell.setTextAndIcon(LocaleController.getString("NekoSettings", R.string.NekoSettings), R.drawable.baseline_extension_24, true); - } else if (position == filtersRow) { - textCell.setTextAndIcon(LocaleController.getString("Filters", R.string.Filters), R.drawable.baseline_folder_24, true); - } else if (position == questionRow) { - textCell.setTextAndIcon(LocaleController.getString("NekoXUpdatesChannel", R.string.NekoXUpdatesChannel), R.drawable.baseline_bullhorn_24, true); - } else if (position == faqRow) { - textCell.setTextAndIcon(LocaleController.getString("NekoXFaq", R.string.NekoXFaq), R.drawable.baseline_help_24, true); - } else if (position == sendLogsRow) { - textCell.setTextAndIcon(LocaleController.getString("DebugSendLogs", R.string.DebugSendLogs), R.drawable.baseline_bug_report_24, true); - } else if (position == clearLogsRow) { - textCell.setTextAndIcon(LocaleController.getString("DebugClearLogs", R.string.DebugClearLogs), R.drawable.baseline_delete_sweep_24, switchBackendRow != -1); - } else if (position == switchBackendRow) { - textCell.setText("Switch Backend", false); - } - break; - } - case 4: { - HeaderCell headerCell = (HeaderCell) holder.itemView; - if (position == settingsSectionRow2) { - headerCell.setText(LocaleController.getString("SETTINGS", R.string.SETTINGS)); - } else if (position == numberSectionRow) { - headerCell.setText(LocaleController.getString("Account", R.string.Account)); - } else if (position == helpHeaderRow) { - headerCell.setText(LocaleController.getString("SettingsHelp", R.string.SettingsHelp)); - } else if (position == debugHeaderRow) { - headerCell.setText(LocaleController.getString("SettingsDebug", R.string.SettingsDebug)); - } - break; - } - case 6: { - TextDetailCell textCell = (TextDetailCell) holder.itemView; - - if (position == numberRow) { - TLRPC.User user = UserConfig.getInstance(currentAccount).getCurrentUser(); - String value; - if (user != null && user.phone != null && user.phone.length() != 0) { - value = PhoneFormat.getInstance().format("+" + user.phone); - } else { - value = LocaleController.getString("NumberUnknown", R.string.NumberUnknown); - } - textCell.setTextAndValue(value, LocaleController.getString("TapToChangePhone", R.string.TapToChangePhone), true); - } else if (position == usernameRow) { - TLRPC.User user = UserConfig.getInstance(currentAccount).getCurrentUser(); - String value; - if (user != null && !TextUtils.isEmpty(user.username)) { - value = "@" + user.username; - } else { - value = LocaleController.getString("UsernameEmpty", R.string.UsernameEmpty); - } - textCell.setTextAndValue(value, LocaleController.getString("Username", R.string.Username), true); - } else if (position == bioRow) { - String value; - if (userInfo == null || !TextUtils.isEmpty(userInfo.about)) { - value = userInfo == null ? LocaleController.getString("Loading", R.string.Loading) : userInfo.about; - textCell.setTextWithEmojiAndValue(value, LocaleController.getString("UserBio", R.string.UserBio), false); - currentBio = userInfo != null ? userInfo.about : null; - } else { - textCell.setTextAndValue(LocaleController.getString("UserBio", R.string.UserBio), LocaleController.getString("UserBioDetail", R.string.UserBioDetail), false); - currentBio = null; - } - } - break; - } - } - } - - @Override - public boolean isEnabled(RecyclerView.ViewHolder holder) { - int position = holder.getAdapterPosition(); - return position == notificationRow || position == numberRow || position == privacyRow || - position == languageRow || position == usernameRow || position == bioRow || - position == versionRow || position == dataRow || position == chatRow || position == stickersRow || - position == questionRow || position == devicesRow || position == filtersRow || - position == faqRow || position == policyRow || position == sendLogsRow || - position == clearLogsRow || position == switchBackendRow || position == nekoRow; - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = null; - switch (viewType) { - case 0: - view = new EmptyCell(mContext, LocaleController.isRTL ? 46 : 36); - break; - case 1: { - view = new ShadowSectionCell(mContext); - Drawable drawable = Theme.getThemedDrawable(mContext, R.drawable.greydivider, Theme.key_windowBackgroundGrayShadow); - CombinedDrawable combinedDrawable = new CombinedDrawable(new ColorDrawable(Theme.getColor(Theme.key_windowBackgroundGray)), drawable); - combinedDrawable.setFullsize(true); - view.setBackgroundDrawable(combinedDrawable); - break; - } - case 2: - view = new TextCell(mContext); - break; - case 4: - view = new HeaderCell(mContext, 23); - break; - case 5: { - TextInfoPrivacyCell cell = new TextInfoPrivacyCell(mContext, 10); - cell.getTextView().setGravity(Gravity.CENTER_HORIZONTAL); - cell.getTextView().setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText3)); - cell.getTextView().setMovementMethod(null); - cell.setBackgroundDrawable(Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow)); - - cell.setText("Nekogram X v" + BuildConfig.VERSION_NAME + " " + FileUtil.getAbi() + " " + BuildConfig.BUILD_TYPE); - - cell.getTextView().setPadding(0, AndroidUtilities.dp(14), 0, AndroidUtilities.dp(14)); - view = cell; - Drawable drawable = Theme.getThemedDrawable(mContext, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow); - CombinedDrawable combinedDrawable = new CombinedDrawable(new ColorDrawable(Theme.getColor(Theme.key_windowBackgroundGray)), drawable); - combinedDrawable.setFullsize(true); - view.setBackgroundDrawable(combinedDrawable); - break; - } - case 6: - view = new TextDetailCell(mContext); - break; - } - view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)); - return new RecyclerListView.Holder(view); - } - - @Override - public int getItemViewType(int position) { - if (position == emptyRow) { - return 0; - } else if (position == settingsSectionRow || position == devicesSectionRow || position == helpSectionCell) { - return 1; - } else if (position == notificationRow || position == privacyRow || position == languageRow || - position == dataRow || position == chatRow || position == questionRow || - position == devicesRow || position == filtersRow || position == faqRow || - position == policyRow || position == sendLogsRow || position == clearLogsRow || - position == switchBackendRow || position == nekoRow || position == stickersRow) { - return 2; - } else if (position == versionRow) { - return 5; - } else if (position == numberRow || position == usernameRow || position == bioRow) { - return 6; - } else if (position == settingsSectionRow2 || position == numberSectionRow || position == helpHeaderRow || - position == debugHeaderRow) { - return 4; - } else { - return 2; - } - } - - } - - @Override - public ArrayList getThemeDescriptions() { - ArrayList themeDescriptions = new ArrayList<>(); - - themeDescriptions.add(new ThemeDescription(fragmentView, 0, null, null, null, null, Theme.key_windowBackgroundWhite)); - themeDescriptions.add(new ThemeDescription(fragmentView, 0, null, null, null, null, Theme.key_windowBackgroundGray)); - - themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_avatar_backgroundActionBarBlue)); - themeDescriptions.add(new ThemeDescription(listView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_avatar_backgroundActionBarBlue)); - themeDescriptions.add(new ThemeDescription(topView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_avatar_backgroundActionBarBlue)); - themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_avatar_actionBarIconBlue)); - themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle)); - themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_avatar_actionBarSelectorBlue)); - themeDescriptions.add(new ThemeDescription(nameTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_profile_title)); - themeDescriptions.add(new ThemeDescription(onlineTextView, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_profile_status)); - themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUBACKGROUND, null, null, null, null, Theme.key_actionBarDefaultSubmenuBackground)); - themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUITEM, null, null, null, null, Theme.key_actionBarDefaultSubmenuItem)); - themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SUBMENUITEM | ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_actionBarDefaultSubmenuItemIcon)); - - themeDescriptions.add(new ThemeDescription(listView, ThemeDescription.FLAG_SELECTOR, null, null, null, null, Theme.key_listSelector)); - - themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider)); - - themeDescriptions.add(new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); - themeDescriptions.add(new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{ShadowSectionCell.class}, null, null, null, Theme.key_windowBackgroundGray)); - - themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{TextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); - themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{TextCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteValueText)); - themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{TextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon)); - - themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); - - themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{TextDetailCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); - themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{TextDetailCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2)); - - themeDescriptions.add(new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow)); - themeDescriptions.add(new ThemeDescription(listView, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGray)); - themeDescriptions.add(new ThemeDescription(listView, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4)); - - themeDescriptions.add(new ThemeDescription(avatarImage, 0, null, null, Theme.avatarDrawables, null, Theme.key_avatar_text)); - themeDescriptions.add(new ThemeDescription(avatarImage, 0, null, null, new Drawable[]{avatarDrawable}, null, Theme.key_avatar_backgroundInProfileBlue)); - - themeDescriptions.add(new ThemeDescription(writeButton, ThemeDescription.FLAG_IMAGECOLOR, null, null, null, null, Theme.key_profile_actionIcon)); - themeDescriptions.add(new ThemeDescription(writeButton, ThemeDescription.FLAG_BACKGROUNDFILTER, null, null, null, null, Theme.key_profile_actionBackground)); - themeDescriptions.add(new ThemeDescription(writeButton, ThemeDescription.FLAG_BACKGROUNDFILTER | ThemeDescription.FLAG_DRAWABLESELECTEDSTATE, null, null, null, null, Theme.key_profile_actionPressedBackground)); - - themeDescriptions.add(new ThemeDescription(searchListView, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader)); - - themeDescriptions.add(new ThemeDescription(searchListView, 0, new Class[]{GraySectionCell.class}, new String[]{"textView"}, null, null, null, Theme.key_graySectionText)); - themeDescriptions.add(new ThemeDescription(searchListView, ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{GraySectionCell.class}, null, null, null, Theme.key_graySection)); - - themeDescriptions.add(new ThemeDescription(searchListView, 0, new Class[]{SettingsSearchCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); - themeDescriptions.add(new ThemeDescription(searchListView, 0, new Class[]{SettingsSearchCell.class}, new String[]{"valueTextView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText2)); - themeDescriptions.add(new ThemeDescription(searchListView, 0, new Class[]{SettingsSearchCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon)); - - return themeDescriptions; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/StatisticActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/StatisticActivity.java index 9fc72ee49..23f8118e0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/StatisticActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/StatisticActivity.java @@ -25,6 +25,7 @@ import androidx.annotation.NonNull; import androidx.collection.ArraySet; import androidx.core.graphics.ColorUtils; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -39,9 +40,12 @@ import org.telegram.messenger.MessageObject; import org.telegram.messenger.MessagesController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; +import org.telegram.messenger.UserConfig; import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.AlertDialog; import org.telegram.ui.ActionBar.BackDrawable; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.Theme; @@ -49,6 +53,8 @@ import org.telegram.ui.ActionBar.ThemeDescription; import org.telegram.ui.Cells.EmptyCell; import org.telegram.ui.Cells.HeaderCell; import org.telegram.ui.Cells.LoadingCell; +import org.telegram.ui.Cells.ManageChatTextCell; +import org.telegram.ui.Cells.ManageChatUserCell; import org.telegram.ui.Cells.ShadowSectionCell; import org.telegram.ui.Cells.StatisticPostInfoCell; import org.telegram.ui.Charts.BarChartView; @@ -84,10 +90,13 @@ public class StatisticActivity extends BaseFragment implements NotificationCente private final TLRPC.ChatFull chat; - private OverviewData overviewData; + //mutual private ChartViewData growthData; - private ChartViewData followersData; private ChartViewData topHoursData; + + //channels + private OverviewChannelData overviewChannelData; + private ChartViewData followersData; private ChartViewData interactionsData; private ChartViewData ivInteractionsData; private ChartViewData viewsBySourceData; @@ -95,6 +104,19 @@ public class StatisticActivity extends BaseFragment implements NotificationCente private ChartViewData languagesData; private ChartViewData notificationsData; + //chats + private OverviewChatData overviewChatData; + private ChartViewData groupMembersData; + private ChartViewData newMembersBySourceData; + private ChartViewData membersLanguageData; + private ChartViewData messagesData; + private ChartViewData actionsData; + private ChartViewData topDayOfWeeksData; + private ArrayList topMembersAll = new ArrayList<>(); + private ArrayList topMembersVisible = new ArrayList<>(); + private ArrayList topInviters = new ArrayList<>(); + private ArrayList topAdmins = new ArrayList<>(); + ChatAvatarContainer avatarContainer; private RecyclerListView recyclerListView; @@ -103,14 +125,21 @@ public class StatisticActivity extends BaseFragment implements NotificationCente private RLottieImageView imageView; private Adapter adapter; + private RecyclerView.ItemAnimator animator; private ZoomCancelable lastCancelable; private BaseChartView.SharedUiComponents sharedUi; private LinearLayout progressLayout; + private final boolean isMegagroup; + private long maxDateOverview; + private long minDateOverview; + + private AlertDialog[] progressDialog = new AlertDialog[1]; public StatisticActivity(Bundle args) { super(args); int chatId = args.getInt("chat_id"); + isMegagroup = args.getBoolean("is_megagroup"); this.chat = getMessagesController().getChatFull(chatId); } @@ -134,12 +163,21 @@ public class StatisticActivity extends BaseFragment implements NotificationCente public boolean onFragmentCreate() { getNotificationCenter().addObserver(this, NotificationCenter.messagesDidLoad); - TLRPC.TL_stats_getBroadcastStats req = new TLRPC.TL_stats_getBroadcastStats(); - req.channel = MessagesController.getInstance(currentAccount).getInputChannel(chat.id); - int reqId = getConnectionsManager().sendRequest(req, (response, error) -> { - final ChartViewData[] chartsViewData = new ChartViewData[9]; + TLObject req; + if (isMegagroup) { + TLRPC.TL_stats_getMegagroupStats getMegagroupStats = new TLRPC.TL_stats_getMegagroupStats(); + req = getMegagroupStats; + getMegagroupStats.channel = MessagesController.getInstance(currentAccount).getInputChannel(chat.id); + } else { + TLRPC.TL_stats_getBroadcastStats getBroadcastStats = new TLRPC.TL_stats_getBroadcastStats(); + req = getBroadcastStats; + getBroadcastStats.channel = MessagesController.getInstance(currentAccount).getInputChannel(chat.id); + } + + int reqId = getConnectionsManager().sendRequest(req, (response, error) -> { if (response instanceof TLRPC.TL_stats_broadcastStats) { + final ChartViewData[] chartsViewData = new ChartViewData[9]; TLRPC.TL_stats_broadcastStats stats = (TLRPC.TL_stats_broadcastStats) response; chartsViewData[0] = createViewData(stats.iv_interactions_graph, LocaleController.getString("IVInteractionsChartTitle", R.string.IVInteractionsChartTitle), 1); @@ -149,12 +187,15 @@ public class StatisticActivity extends BaseFragment implements NotificationCente chartsViewData[4] = createViewData(stats.growth_graph, LocaleController.getString("GrowthChartTitle", R.string.GrowthChartTitle), 0); chartsViewData[5] = createViewData(stats.views_by_source_graph, LocaleController.getString("ViewsBySourceChartTitle", R.string.ViewsBySourceChartTitle), 2); chartsViewData[6] = createViewData(stats.new_followers_by_source_graph, LocaleController.getString("NewFollowersBySourceChartTitle", R.string.NewFollowersBySourceChartTitle), 2); - chartsViewData[7] = createViewData(stats.languages_graph, LocaleController.getString("LanguagesChartTitle", R.string.LanguagesChartTitle), 4); + chartsViewData[7] = createViewData(stats.languages_graph, LocaleController.getString("LanguagesChartTitle", R.string.LanguagesChartTitle), 4, true); chartsViewData[8] = createViewData(stats.mute_graph, LocaleController.getString("NotificationsChartTitle", R.string.NotificationsChartTitle), 0); - overviewData = new OverviewData(stats); + overviewChannelData = new OverviewChannelData(stats); + maxDateOverview = stats.period.max_date * 1000L; + minDateOverview = stats.period.min_date * 1000L; recentPostsAll.clear(); + for (int i = 0; i < stats.recent_message_interactions.size(); i++) { RecentPostInfo recentPostInfo = new RecentPostInfo(); recentPostInfo.counters = stats.recent_message_interactions.get(i); @@ -180,41 +221,107 @@ public class StatisticActivity extends BaseFragment implements NotificationCente languagesData = chartsViewData[7]; notificationsData = chartsViewData[8]; - if (adapter != null) { - adapter.update(); - adapter.notifyDataSetChanged(); - } - initialLoading = false; - if (progressLayout != null && progressLayout.getVisibility() == View.VISIBLE) { - AndroidUtilities.cancelRunOnUIThread(showProgressbar); - progressLayout.animate().alpha(0).setDuration(230).setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - progressLayout.setVisibility(View.GONE); - } - }); - recyclerListView.setVisibility(View.VISIBLE); - recyclerListView.setAlpha(0); - recyclerListView.animate().alpha(1).setDuration(230).start(); - - for (ChartViewData data : chartsViewData) { - if (data != null && data.chartData == null && data.token != null) { - data.load(currentAccount, classGuid, chat.stats_dc, recyclerListView, adapter, diffUtilsCallback); - } - } - } + dataLoaded(chartsViewData); }); } + + if (response instanceof TLRPC.TL_stats_megagroupStats) { + final ChartViewData[] chartsViewData = new ChartViewData[8]; + TLRPC.TL_stats_megagroupStats stats = (TLRPC.TL_stats_megagroupStats) response; + + chartsViewData[0] = createViewData(stats.growth_graph, LocaleController.getString("GrowthChartTitle", R.string.GrowthChartTitle), 0); + chartsViewData[1] = createViewData(stats.members_graph, LocaleController.getString("GroupMembersChartTitle", R.string.GroupMembersChartTitle), 0); + chartsViewData[2] = createViewData(stats.new_members_by_source_graph, LocaleController.getString("NewMembersBySourceChartTitle", R.string.NewMembersBySourceChartTitle), 2); + chartsViewData[3] = createViewData(stats.languages_graph, LocaleController.getString("MembersLanguageChartTitle", R.string.MembersLanguageChartTitle), 4, true); + chartsViewData[4] = createViewData(stats.messages_graph, LocaleController.getString("MessagesChartTitle", R.string.MessagesChartTitle), 2); + chartsViewData[5] = createViewData(stats.actions_graph, LocaleController.getString("ActionsChartTitle", R.string.ActionsChartTitle), 1); + chartsViewData[6] = createViewData(stats.top_hours_graph, LocaleController.getString("TopHoursChartTitle", R.string.TopHoursChartTitle), 0); + chartsViewData[7] = createViewData(stats.weekdays_graph, LocaleController.getString("TopDaysOfWeekChartTitle", R.string.TopDaysOfWeekChartTitle), 4); + + overviewChatData = new OverviewChatData(stats); + maxDateOverview = stats.period.max_date * 1000L; + minDateOverview = stats.period.min_date * 1000L; + + if (stats.top_posters != null && !stats.top_posters.isEmpty()) { + for (int i = 0; i < stats.top_posters.size(); i++) { + MemberData data = MemberData.from(stats.top_posters.get(i), stats.users); + if (topMembersVisible.size() < 10) { + topMembersVisible.add(data); + } + topMembersAll.add(data); + } + if (topMembersAll.size() - topMembersVisible.size() < 2) { + topMembersVisible.clear(); + topMembersVisible.addAll(topMembersAll); + } + } + + if (stats.top_admins != null && !stats.top_admins.isEmpty()) { + for (int i = 0; i < stats.top_admins.size(); i++) { + topAdmins.add(MemberData.from(stats.top_admins.get(i), stats.users)); + } + } + + if (stats.top_inviters != null && !stats.top_inviters.isEmpty()) { + for (int i = 0; i < stats.top_inviters.size(); i++) { + topInviters.add(MemberData.from(stats.top_inviters.get(i), stats.users)); + } + } + + AndroidUtilities.runOnUIThread(() -> { + growthData = chartsViewData[0]; + groupMembersData = chartsViewData[1]; + newMembersBySourceData = chartsViewData[2]; + membersLanguageData = chartsViewData[3]; + messagesData = chartsViewData[4]; + actionsData = chartsViewData[5]; + topHoursData = chartsViewData[6]; + topDayOfWeeksData = chartsViewData[7]; + + dataLoaded(chartsViewData); + }); + } }, null, null, 0, chat.stats_dc, ConnectionsManager.ConnectionTypeGeneric, true); getConnectionsManager().bindRequestToGuid(reqId, classGuid); return super.onFragmentCreate(); } + private void dataLoaded(ChartViewData[] chartsViewData) { + if (adapter != null) { + adapter.update(); + recyclerListView.setItemAnimator(null); + adapter.notifyDataSetChanged(); + } + initialLoading = false; + if (progressLayout != null && progressLayout.getVisibility() == View.VISIBLE) { + AndroidUtilities.cancelRunOnUIThread(showProgressbar); + progressLayout.animate().alpha(0).setDuration(230).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + progressLayout.setVisibility(View.GONE); + } + }); + recyclerListView.setVisibility(View.VISIBLE); + recyclerListView.setAlpha(0); + recyclerListView.animate().alpha(1).setDuration(230).start(); + + for (ChartViewData data : chartsViewData) { + if (data != null && data.chartData == null && data.token != null) { + data.load(currentAccount, classGuid, chat.stats_dc, recyclerListView, adapter, diffUtilsCallback); + } + } + } + } + @Override public void onFragmentDestroy() { getNotificationCenter().removeObserver(this, NotificationCenter.messagesDidLoad); + if (progressDialog[0] != null) { + progressDialog[0].dismiss(); + progressDialog[0] = null; + } super.onFragmentDestroy(); } @@ -257,6 +364,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente loadMessages(); } if (adapter != null) { + recyclerListView.setItemAnimator(null); diffUtilsCallback.update(); } } @@ -317,6 +425,12 @@ public class StatisticActivity extends BaseFragment implements NotificationCente recyclerListView.setAdapter(adapter); layoutManager = new LinearLayoutManager(context); recyclerListView.setLayoutManager(layoutManager); + animator = new DefaultItemAnimator() { + @Override + protected long getAddAnimationDelay(long removeDuration, long moveDuration, long changeDuration) { + return removeDuration; + } + }; recyclerListView.setItemAnimator(null); recyclerListView.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -340,9 +454,46 @@ public class StatisticActivity extends BaseFragment implements NotificationCente bundle.putBoolean("need_remove_previous_same_chat_activity", false); ChatActivity chatActivity = new ChatActivity(bundle); presentFragment(chatActivity, false); + } else if (position >= adapter.topAdminsStartRow && position <= adapter.topAdminsEndRow) { + int i = position - adapter.topAdminsStartRow; + topAdmins.get(i).onClick(this); + } else if (position >= adapter.topMembersStartRow && position <= adapter.topMembersEndRow) { + int i = position - adapter.topMembersStartRow; + topMembersVisible.get(i).onClick(this); + } else if (position >= adapter.topInviterStartRow && position <= adapter.topInviterEndRow) { + int i = position - adapter.topInviterStartRow; + topInviters.get(i).onClick(this); + } else if (position == adapter.expandTopMembersRow) { + int newCount = topMembersAll.size() - topMembersVisible.size(); + int p = adapter.expandTopMembersRow; + topMembersVisible.clear(); + topMembersVisible.addAll(topMembersAll); + if (adapter != null) { + adapter.update(); + recyclerListView.setItemAnimator(animator); + adapter.notifyItemRangeInserted(p + 1, newCount); + adapter.notifyItemRemoved(p); + } } - }); + + recyclerListView.setOnItemLongClickListener((view, position) -> { + if (position >= adapter.topAdminsStartRow && position <= adapter.topAdminsEndRow) { + int i = position - adapter.topAdminsStartRow; + topAdmins.get(i).onLongClick(chat, this, progressDialog); + return true; + } else if (position >= adapter.topMembersStartRow && position <= adapter.topMembersEndRow) { + int i = position - adapter.topMembersStartRow; + topMembersVisible.get(i).onLongClick(chat, this, progressDialog); + return true; + } else if (position >= adapter.topInviterStartRow && position <= adapter.topInviterEndRow) { + int i = position - adapter.topInviterStartRow; + topInviters.get(i).onLongClick(chat, this, progressDialog); + return true; + } + return false; + }); + frameLayout.addView(recyclerListView); avatarContainer = new ChatAvatarContainer(context, null, false); @@ -386,7 +537,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente return fragmentView; } - private ChartViewData createViewData(TLRPC.StatsGraph graph, String title, int graphType) { + private ChartViewData createViewData(TLRPC.StatsGraph graph, String title, int graphType, boolean isLanguages) { if (graph == null || graph instanceof TLRPC.TL_statsGraphError) { return null; } @@ -394,7 +545,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente if (graph instanceof TLRPC.TL_statsGraph) { String json = ((TLRPC.TL_statsGraph) graph).json.data; try { - viewData.chartData = createChartData(new JSONObject(json), graphType); + viewData.chartData = createChartData(new JSONObject(json), graphType, isLanguages); viewData.zoomToken = ((TLRPC.TL_statsGraph) graph).zoom_token; if (viewData.chartData == null || viewData.chartData.x == null || viewData.chartData.x.length < 2) { viewData.isEmpty = true; @@ -415,7 +566,11 @@ public class StatisticActivity extends BaseFragment implements NotificationCente return viewData; } - private static ChartData createChartData(JSONObject jsonObject, int graphType) throws JSONException { + private ChartViewData createViewData(TLRPC.StatsGraph graph, String title, int graphType) { + return createViewData(graph, title, graphType, false); + } + + private static ChartData createChartData(JSONObject jsonObject, int graphType, boolean isLanguages) throws JSONException { if (graphType == 0) { return new ChartData(jsonObject); } else if (graphType == 1) { @@ -423,7 +578,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente } else if (graphType == 2) { return new StackBarChartData(jsonObject); } else if (graphType == 4) { - return new StackLinearChartData(jsonObject); + return new StackLinearChartData(jsonObject, isLanguages); } return null; } @@ -433,6 +588,9 @@ public class StatisticActivity extends BaseFragment implements NotificationCente int overviewHeaderCell = -1; int overviewCell; int growCell = -1; + int progressCell = -1; + + // channels int folowersCell = -1; int topHourseCell = -1; int interactionsCell = -1; @@ -445,33 +603,59 @@ public class StatisticActivity extends BaseFragment implements NotificationCente int recentPostsHeaderCell = -1; int recentPostsStartRow = -1; int recentPostsEndRow = -1; - int progressCell = -1; - int emptyCell = -1; + + //megagroup + int groupMembersCell = -1; + int newMembersBySourceCell = -1; + int membersLanguageCell = -1; + int messagesCell = -1; + int actionsCell = -1; + int topDayOfWeeksCell = -1; + int topMembersHeaderCell = -1; + int topMembersStartRow = -1; + int topMembersEndRow = -1; + int topAdminsHeaderCell = -1; + int topAdminsStartRow = -1; + int topAdminsEndRow = -1; + int topInviterHeaderCell = -1; + int topInviterStartRow = -1; + int topInviterEndRow = -1; + int expandTopMembersRow = -1; + ArraySet shadowDivideCells = new ArraySet<>(); + ArraySet emptyCells = new ArraySet<>(); int count; @Override public int getItemViewType(int position) { - if (position == growCell || position == folowersCell || position == topHourseCell || position == notificationsCell) { + if (position == growCell || position == folowersCell || position == topHourseCell || position == notificationsCell || position == actionsCell || position == groupMembersCell) { return 0; } else if (position == interactionsCell || position == ivInteractionsCell) { return 1; - } else if (position == viewsBySourceCell || position == newFollowersBySourceCell) { + } else if (position == viewsBySourceCell || position == newFollowersBySourceCell || position == newMembersBySourceCell || position == messagesCell) { return 2; - } else if (position == languagesCell) { + } else if (position == languagesCell || position == membersLanguageCell || position == topDayOfWeeksCell) { return 4; } else if (position >= recentPostsStartRow && position <= recentPostsEndRow) { return 9; } else if (position == progressCell) { return 11; - } else if (position == emptyCell) { + } else if (emptyCells.contains(position)) { return 12; - } else if (position == recentPostsHeaderCell || position == overviewHeaderCell) { + } else if (position == recentPostsHeaderCell || position == overviewHeaderCell || + position == topAdminsHeaderCell || position == topMembersHeaderCell || position == topInviterHeaderCell) { return 13; } else if (position == overviewCell) { return 14; + } else if ((position >= topAdminsStartRow && position <= topAdminsEndRow) || + (position >= topMembersStartRow && position <= topMembersEndRow) || + (position >= topInviterStartRow && position <= topInviterEndRow)) { + return 9; + } + if (position == expandTopMembersRow) { + return 15; } else { return 10; } @@ -500,6 +684,18 @@ public class StatisticActivity extends BaseFragment implements NotificationCente return 8; } else if (position == languagesCell) { return 9; + } else if (position == groupMembersCell) { + return 10; + } else if (position == newMembersBySourceCell) { + return 11; + } else if (position == membersLanguageCell) { + return 12; + } else if (position == messagesCell) { + return 13; + } else if (position == actionsCell) { + return 14; + } else if (position == topDayOfWeeksCell) { + return 15; } return super.getItemId(position); } @@ -535,7 +731,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente } else if (viewType == 12) { v = new EmptyCell(parent.getContext(), AndroidUtilities.dp(15)); } else if (viewType == 13) { - HeaderCell headerCell = new HeaderCell(parent.getContext(), Theme.key_dialogTextBlack, 16, 15, false) { + ChartHeaderView headerCell = new ChartHeaderView(parent.getContext()) { @Override protected void onDraw(Canvas canvas) { if (getTranslationY() != 0) { @@ -545,10 +741,14 @@ public class StatisticActivity extends BaseFragment implements NotificationCente } }; headerCell.setWillNotDraw(false); - headerCell.setPadding(headerCell.getPaddingLeft(), headerCell.getTop(), headerCell.getRight(), AndroidUtilities.dp(8)); + headerCell.setPadding(headerCell.getPaddingLeft(), AndroidUtilities.dp(16), headerCell.getRight(), AndroidUtilities.dp(16)); v = headerCell; } else if (viewType == 14) { v = new OverviewCell(parent.getContext()); + } else if (viewType == 15) { + v = new ManageChatTextCell(parent.getContext()); + v.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); + ((ManageChatTextCell) v).setColors(Theme.key_windowBackgroundWhiteBlueIcon, Theme.key_windowBackgroundWhiteBlueButton); } else { v = new ShadowSectionCell(parent.getContext(), 12, Theme.getColor(Theme.key_windowBackgroundGray)); } @@ -577,23 +777,62 @@ public class StatisticActivity extends BaseFragment implements NotificationCente data = topHoursData; } else if (notificationsCell == position) { data = notificationsData; + } else if (groupMembersCell == position) { + data = groupMembersData; + } else if (newMembersBySourceCell == position) { + data = newMembersBySourceData; + } else if (membersLanguageCell == position) { + data = membersLanguageData; + } else if (messagesCell == position) { + data = messagesData; + } else if (actionsCell == position) { + data = actionsData; + } else if (topDayOfWeeksCell == position) { + data = topDayOfWeeksData; } else { data = languagesData; } ((ChartCell) holder.itemView).updateData(data, false); } else if (type == 9) { - int i = position - recentPostsStartRow; - ((StatisticPostInfoCell) holder.itemView).setData(recentPostsLoaded.get(i)); - } else if (type == 13) { - HeaderCell headerCell = (HeaderCell) holder.itemView; - if (position == overviewHeaderCell) { - headerCell.setText(LocaleController.getString("StatisticOverview", R.string.StatisticOverview)); + if (isMegagroup) { + if (position >= topAdminsStartRow && position <= topAdminsEndRow) { + int i = position - topAdminsStartRow; + ((StatisticPostInfoCell) holder.itemView).setData(topAdmins.get(i)); + } else if (position >= topMembersStartRow && position <= topMembersEndRow) { + int i = position - topMembersStartRow; + ((StatisticPostInfoCell) holder.itemView).setData(topMembersVisible.get(i)); + } else if (position >= topInviterStartRow && position <= topInviterEndRow) { + int i = position - topInviterStartRow; + ((StatisticPostInfoCell) holder.itemView).setData(topInviters.get(i)); + } } else { - headerCell.setText(LocaleController.getString("RecentPosts", R.string.RecentPosts)); + int i = position - recentPostsStartRow; + ((StatisticPostInfoCell) holder.itemView).setData(recentPostsLoaded.get(i)); + } + } else if (type == 13) { + ChartHeaderView headerCell = (ChartHeaderView) holder.itemView; + headerCell.setDates(minDateOverview, maxDateOverview); + if (position == overviewHeaderCell) { + headerCell.setTitle(LocaleController.getString("StatisticOverview", R.string.StatisticOverview)); + } else if (position == topAdminsHeaderCell) { + headerCell.setTitle(LocaleController.getString("TopAdmins", R.string.TopAdmins)); + } else if (position == topInviterHeaderCell) { + headerCell.setTitle(LocaleController.getString("TopInviters", R.string.TopInviters)); + } else if (position == topMembersHeaderCell) { + headerCell.setTitle(LocaleController.getString("TopMembers", R.string.TopMembers)); + } else { + headerCell.setTitle(LocaleController.getString("RecentPosts", R.string.RecentPosts)); } } else if (type == 14) { OverviewCell overviewCell = (OverviewCell) holder.itemView; - overviewCell.setData(overviewData); + if (isMegagroup) { + overviewCell.setData(overviewChatData); + } else { + overviewCell.setData(overviewChannelData); + } + } else if (type == 15) { + ManageChatTextCell manageChatTextCell = (ManageChatTextCell) holder.itemView; + manageChatTextCell.setText(LocaleController.formatPluralString("ShowVotes", topMembersAll.size() - topMembersVisible.size()), null, R.drawable.arrow_more, false); } } @@ -612,97 +851,211 @@ public class StatisticActivity extends BaseFragment implements NotificationCente recentPostsStartRow = -1; recentPostsEndRow = -1; progressCell = -1; - emptyCell = -1; recentPostsHeaderCell = -1; ivInteractionsCell = -1; topHourseCell = -1; notificationsCell = -1; + groupMembersCell = -1; + newMembersBySourceCell = -1; + membersLanguageCell = -1; + messagesCell = -1; + actionsCell = -1; + topDayOfWeeksCell = -1; + topMembersHeaderCell = -1; + topMembersStartRow = -1; + topMembersEndRow = -1; + topAdminsHeaderCell = -1; + topAdminsStartRow = -1; + topAdminsEndRow = -1; + topInviterHeaderCell = -1; + topInviterStartRow = -1; + topInviterEndRow = -1; + expandTopMembersRow = -1; count = 0; + emptyCells.clear(); shadowDivideCells.clear(); - if (overviewData != null) { - overviewHeaderCell = count++; - overviewCell = count++; - } + if (isMegagroup) { + if (overviewChatData != null) { + overviewHeaderCell = count++; + overviewCell = count++; + } - if (growthData != null && !growthData.isEmpty) { - if (count > 0) { - shadowDivideCells.add(count++); + if (growthData != null && !growthData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + growCell = count++; + } + if (groupMembersData != null && !groupMembersData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + groupMembersCell = count++; + } + if (newMembersBySourceData != null && !newMembersBySourceData.isEmpty && !newMembersBySourceData.isError) { + if (count > 0) { + shadowDivideCells.add(count++); + } + newMembersBySourceCell = count++; + } + if (membersLanguageData != null && !membersLanguageData.isEmpty && !membersLanguageData.isError) { + if (count > 0) { + shadowDivideCells.add(count++); + } + membersLanguageCell = count++; + } + if (messagesData != null && !messagesData.isEmpty && !messagesData.isError) { + if (count > 0) { + shadowDivideCells.add(count++); + } + messagesCell = count++; + } + if (actionsData != null && !actionsData.isEmpty && !actionsData.isError) { + if (count > 0) { + shadowDivideCells.add(count++); + } + actionsCell = count++; + } + if (topHoursData != null && !topHoursData.isEmpty && !topHoursData.isError) { + if (count > 0) { + shadowDivideCells.add(count++); + } + topHourseCell = count++; } - growCell = count++; - } - if (followersData != null && !followersData.isEmpty) { - if (count > 0) { - shadowDivideCells.add(count++); + if (topDayOfWeeksData != null && !topDayOfWeeksData.isEmpty && !topDayOfWeeksData.isError) { + if (count > 0) { + shadowDivideCells.add(count++); + } + topDayOfWeeksCell = count++; } - folowersCell = count++; - } - if (notificationsData != null && !notificationsData.isEmpty) { - if (count > 0) { - shadowDivideCells.add(count++); - } - notificationsCell = count++; - } - if (topHoursData != null && !topHoursData.isEmpty) { - if (count > 0) { - shadowDivideCells.add(count++); - } - topHourseCell = count++; - } - if (viewsBySourceData != null && !viewsBySourceData.isEmpty) { - if (count > 0) { - shadowDivideCells.add(count++); - } - viewsBySourceCell = count++; - } - if (newFollowersBySourceData != null && !newFollowersBySourceData.isEmpty) { - if (count > 0) { - shadowDivideCells.add(count++); - } - newFollowersBySourceCell = count++; - } - if (languagesData != null && !languagesData.isEmpty) { - if (count > 0) { - shadowDivideCells.add(count++); - } - languagesCell = count++; - } - if (interactionsData != null && !interactionsData.isEmpty) { - if (count > 0) { - shadowDivideCells.add(count++); - } - interactionsCell = count++; - } - if ((ivInteractionsData != null && !ivInteractionsData.loading && !ivInteractionsData.isError)) { - if (count > 0) { - shadowDivideCells.add(count++); - } - ivInteractionsCell = count++; - } - shadowDivideCells.add(count++); + if (topMembersVisible.size() > 0) { + if (count > 0) { + shadowDivideCells.add(count++); + } + topMembersHeaderCell = count++; + topMembersStartRow = count++; + count = topMembersEndRow = topMembersStartRow + topMembersVisible.size() - 1; + count++; + if (topMembersVisible.size() != topMembersAll.size()) { + expandTopMembersRow = count++; + } else { + emptyCells.add(count++); + } + } - if (recentPostsAll.size() > 0) { - recentPostsHeaderCell = count++; - recentPostsStartRow = count++; - count = recentPostsEndRow = recentPostsStartRow + recentPostsLoaded.size() - 1; - count++; + if (topAdmins.size() > 0) { + if (count > 0) { + shadowDivideCells.add(count++); + } + topAdminsHeaderCell = count++; + topAdminsStartRow = count++; + count = topAdminsEndRow = topAdminsStartRow + topAdmins.size() - 1; + count++; + emptyCells.add(count++); + } - if (recentPostsLoaded.size() != recentPostsAll.size()) { - progressCell = count++; - } else { - emptyCell = count++; + if (topInviters.size() > 0) { + if (count > 0) { + shadowDivideCells.add(count++); + } + topInviterHeaderCell = count++; + topInviterStartRow = count++; + count = topInviterEndRow = topInviterStartRow + topInviters.size() - 1; + count++; + } + + if (count > 0) { + emptyCells.add(count++); + shadowDivideCells.add(count++); + } + } else { + if (overviewChannelData != null) { + overviewHeaderCell = count++; + overviewCell = count++; + } + + if (growthData != null && !growthData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + growCell = count++; + + } + if (followersData != null && !followersData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + folowersCell = count++; + } + if (notificationsData != null && !notificationsData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + notificationsCell = count++; + } + if (topHoursData != null && !topHoursData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + topHourseCell = count++; + } + if (viewsBySourceData != null && !viewsBySourceData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + viewsBySourceCell = count++; + } + if (newFollowersBySourceData != null && !newFollowersBySourceData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + newFollowersBySourceCell = count++; + } + if (languagesData != null && !languagesData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + languagesCell = count++; + } + if (interactionsData != null && !interactionsData.isEmpty) { + if (count > 0) { + shadowDivideCells.add(count++); + } + interactionsCell = count++; + } + if ((ivInteractionsData != null && !ivInteractionsData.loading && !ivInteractionsData.isError)) { + if (count > 0) { + shadowDivideCells.add(count++); + } + ivInteractionsCell = count++; } shadowDivideCells.add(count++); + + if (recentPostsAll.size() > 0) { + recentPostsHeaderCell = count++; + recentPostsStartRow = count++; + count = recentPostsEndRow = recentPostsStartRow + recentPostsLoaded.size() - 1; + count++; + + if (recentPostsLoaded.size() != recentPostsAll.size()) { + progressCell = count++; + } else { + emptyCells.add(count++); + } + + shadowDivideCells.add(count++); + } } } @Override public boolean isEnabled(RecyclerView.ViewHolder holder) { - return holder.getItemViewType() == 9; + return holder.getItemViewType() == 9 || holder.getItemViewType() == 15; } } @@ -866,7 +1219,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente if (response instanceof TLRPC.TL_statsGraph) { String json = ((TLRPC.TL_statsGraph) response).json.data; try { - childData = createChartData(new JSONObject(json), data.graphType); + childData = createChartData(new JSONObject(json), data.graphType, data == languagesData); } catch (JSONException e) { e.printStackTrace(); } @@ -1002,6 +1355,9 @@ public class StatisticActivity extends BaseFragment implements NotificationCente } private void zoomOut(boolean animated) { + if (data.chartData.x == null) { + return; + } chartHeaderView.zoomOut(chartView, animated); chartView.legendSignatureView.chevron.setAlpha(1f); zoomedChartView.setHeader(null); @@ -1039,12 +1395,16 @@ public class StatisticActivity extends BaseFragment implements NotificationCente chartView.enabled = true; zoomedChartView.enabled = false; - chartView.legendShowing = true; - chartView.moveLegend(); - chartView.animateLegend(true); - chartView.invalidate(); + if (!(chartView instanceof StackLinearChartView)) { + chartView.legendShowing = true; + chartView.moveLegend(); + chartView.animateLegend(true); + chartView.invalidate(); + } else { + chartView.legendShowing = false; + chartView.clearSelection(); + } ((Activity) getContext()).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); - } }); for (CheckBoxHolder checkbox : checkBoxes) { @@ -1091,6 +1451,8 @@ public class StatisticActivity extends BaseFragment implements NotificationCente final float pYPercentage = (((float) min + (max - min)) - chartView.currentMinHeight) / (chartView.currentMaxHeight - chartView.currentMinHeight); + chartView.fillTransitionParams(params); + zoomedChartView.fillTransitionParams(params); ValueAnimator animator = ValueAnimator.ofFloat(in ? 0f : 1f, in ? 1f : 0f); animator.addUpdateListener(animation -> { float fullWidth = (chartView.chartWidth / (chartView.pickerDelegate.pickerEnd - chartView.pickerDelegate.pickerStart)); @@ -1101,10 +1463,11 @@ public class StatisticActivity extends BaseFragment implements NotificationCente params.progress = (float) animation.getAnimatedValue(); zoomedChartView.invalidate(); + zoomedChartView.fillTransitionParams(params); chartView.invalidate(); }); - animator.setDuration(chartType == 4 ? 600 : 400); + animator.setDuration(400); animator.setInterpolator(new FastOutSlowInInterpolator()); return animator; @@ -1145,12 +1508,15 @@ public class StatisticActivity extends BaseFragment implements NotificationCente progressView.setAlpha(1f); progressView.setVisibility(View.VISIBLE); viewData.load(currentAccount, classGuid, chat.stats_dc, recyclerListView, adapter, diffUtilsCallback); + chartView.setData(null); return; } else if (!enterTransition) { progressView.setVisibility(View.GONE); } chartView.setData(viewData.chartData); + chartHeaderView.setUseWeekInterval(viewData == topDayOfWeeksData); + chartView.legendSignatureView.setUseWeek(viewData == topDayOfWeeksData); chartView.legendSignatureView.zoomEnabled = !(data.zoomToken == null && chartType != 4); zoomedChartView.legendSignatureView.zoomEnabled = false; @@ -1330,7 +1696,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente boolean canceled; } - private static class ChartViewData { + private class ChartViewData { public boolean isError; public String errorMessage; @@ -1364,7 +1730,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente if (response instanceof TLRPC.TL_statsGraph) { String json = ((TLRPC.TL_statsGraph) response).json.data; try { - chartData = createChartData(new JSONObject(json), graphType); + chartData = createChartData(new JSONObject(json), graphType, this == languagesData); zoomToken = ((TLRPC.TL_statsGraph) response).zoom_token; if (graphType == 4 && chartData.x != null && chartData.x.length > 0) { long x = chartData.x[chartData.x.length - 1]; @@ -1400,6 +1766,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente } } if (!found) { + recyclerListView.setItemAnimator(null); difCallback.update(); } }); @@ -1468,6 +1835,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente recentPostsLoaded.add(postInfo); } } + recyclerListView.setItemAnimator(null); diffUtilsCallback.update(); }); }); @@ -1485,7 +1853,9 @@ public class StatisticActivity extends BaseFragment implements NotificationCente combinedDrawable.setFullsize(true); child.setBackground(combinedDrawable); } - + if (child instanceof ChartHeaderView) { + ((ChartHeaderView) child).recolor(); + } if (child instanceof OverviewCell) { ((OverviewCell) child).updateColors(); } @@ -1508,6 +1878,13 @@ public class StatisticActivity extends BaseFragment implements NotificationCente int topHourseCell = -1; int notificationsCell = -1; + int groupMembersCell = -1; + int newMembersBySourceCell = -1; + int membersLanguageCell = -1; + int messagesCell = -1; + int actionsCell = -1; + int topDayOfWeeksCell = -1; + int startPosts = -1; int endPosts = -1; @@ -1533,6 +1910,13 @@ public class StatisticActivity extends BaseFragment implements NotificationCente notificationsCell = adapter.notificationsCell; startPosts = adapter.recentPostsStartRow; endPosts = adapter.recentPostsEndRow; + + groupMembersCell = adapter.groupMembersCell; + newMembersBySourceCell = adapter.newMembersBySourceCell; + membersLanguageCell = adapter.membersLanguageCell; + messagesCell = adapter.messagesCell; + actionsCell = adapter.actionsCell; + topDayOfWeeksCell = adapter.topDayOfWeeksCell; } @Override @@ -1574,6 +1958,18 @@ public class StatisticActivity extends BaseFragment implements NotificationCente return true; } else if (oldItemPosition == notificationsCell && newItemPosition == adapter.notificationsCell) { return true; + } else if (oldItemPosition == groupMembersCell && newItemPosition == adapter.groupMembersCell) { + return true; + } else if (oldItemPosition == newMembersBySourceCell && newItemPosition == adapter.newMembersBySourceCell) { + return true; + } else if (oldItemPosition == membersLanguageCell && newItemPosition == adapter.membersLanguageCell) { + return true; + } else if (oldItemPosition == messagesCell && newItemPosition == adapter.messagesCell) { + return true; + } else if (oldItemPosition == actionsCell && newItemPosition == adapter.actionsCell) { + return true; + } else if (oldItemPosition == topDayOfWeeksCell && newItemPosition == adapter.topDayOfWeeksCell) { + return true; } return false; } @@ -1651,9 +2047,9 @@ public class StatisticActivity extends BaseFragment implements NotificationCente arrayList.add(new ThemeDescription(recyclerListView, 0, new Class[]{StatisticPostInfoCell.class}, new String[]{"message"}, null, null, null, Theme.key_dialogTextBlack)); arrayList.add(new ThemeDescription(recyclerListView, 0, new Class[]{StatisticPostInfoCell.class}, new String[]{"views"}, null, null, null, Theme.key_dialogTextBlack)); - arrayList.add(new ThemeDescription(recyclerListView, 0, new Class[]{StatisticPostInfoCell.class}, new String[]{"shares"}, null, null, null, Theme.key_dialogTextGray4)); - arrayList.add(new ThemeDescription(recyclerListView, 0, new Class[]{StatisticPostInfoCell.class}, new String[]{"date"}, null, null, null, Theme.key_dialogTextGray4)); - arrayList.add(new ThemeDescription(recyclerListView, 0, new Class[]{HeaderCell.class}, new String[]{"textView"}, null, null, null, Theme.key_dialogTextBlack)); + arrayList.add(new ThemeDescription(recyclerListView, 0, new Class[]{StatisticPostInfoCell.class}, new String[]{"shares"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText3)); + arrayList.add(new ThemeDescription(recyclerListView, 0, new Class[]{StatisticPostInfoCell.class}, new String[]{"date"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText3)); + arrayList.add(new ThemeDescription(recyclerListView, 0, new Class[]{ChartHeaderView.class}, new String[]{"textView"}, null, null, null, Theme.key_dialogTextBlack)); arrayList.add(new ThemeDescription(null, 0, null, null, null, null, themeDelegate, Theme.key_dialogTextBlack)); arrayList.add(new ThemeDescription(null, 0, null, null, null, null, themeDelegate, Theme.key_statisticChartSignature)); @@ -1675,47 +2071,78 @@ public class StatisticActivity extends BaseFragment implements NotificationCente arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); arrayList.add(new ThemeDescription(avatarContainer != null ? avatarContainer.getTitleTextView() : null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_player_actionBarTitle)); arrayList.add(new ThemeDescription(avatarContainer != null ? avatarContainer.getSubtitleTextView() : null, ThemeDescription.FLAG_TEXTCOLOR | ThemeDescription.FLAG_CHECKTAG, null, null, null, null, Theme.key_player_actionBarSubtitle, null)); + arrayList.add(new ThemeDescription(null, 0, null, null, null, themeDelegate, Theme.key_statisticChartLineEmpty)); + arrayList.add(new ThemeDescription(recyclerListView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); + arrayList.add(new ThemeDescription(recyclerListView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayIcon)); + arrayList.add(new ThemeDescription(recyclerListView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueButton)); + arrayList.add(new ThemeDescription(recyclerListView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteBlueIcon)); + arrayList.add(new ThemeDescription(recyclerListView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"imageView"}, null, null, null, Theme.key_windowBackgroundWhiteRedText5)); + arrayList.add(new ThemeDescription(recyclerListView, ThemeDescription.FLAG_CHECKTAG, new Class[]{ManageChatTextCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteRedText5)); + arrayList.add(new ThemeDescription(recyclerListView, ThemeDescription.FLAG_CELLBACKGROUNDCOLOR, new Class[]{ManageChatUserCell.class, ManageChatTextCell.class, HeaderCell.class, TextView.class, PeopleNearbyActivity.HintInnerCell.class}, null, null, null, Theme.key_windowBackgroundWhite)); - for (int i = 0; i < 9; i++) { - ChartViewData chartViewData; - if (i == 0) { - chartViewData = growthData; - } else if (i == 1) { - chartViewData = followersData; - } else if (i == 2) { - chartViewData = interactionsData; - } else if (i == 3) { - chartViewData = ivInteractionsData; - } else if (i == 4) { - chartViewData = viewsBySourceData; - } else if (i == 5) { - chartViewData = newFollowersBySourceData; - } else if (i == 6) { - chartViewData = notificationsData; - } else if (i == 7) { - chartViewData = topHoursData; - } else { - chartViewData = languagesData; - } - - if (chartViewData != null && chartViewData.chartData != null) { - for (ChartData.Line l : chartViewData.chartData.lines) { - if (l.colorKey != null) { - if (!Theme.hasThemeKey(l.colorKey)) { - Theme.setColor(l.colorKey, Theme.isCurrentThemeNight() ? l.colorDark : l.color, false); - Theme.setDefaultColor(l.colorKey, l.color); - } - arrayList.add(new ThemeDescription(null, 0, null, null, null, themeDelegate, l.colorKey)); - } + if (isMegagroup) { + for (int i = 0; i < 6; i++) { + ChartViewData chartViewData; + if (i == 0) { + chartViewData = growthData; + } else if (i == 1) { + chartViewData = groupMembersData; + } else if (i == 2) { + chartViewData = newMembersBySourceData; + } else if (i == 3) { + chartViewData = membersLanguageData; + } else if (i == 4) { + chartViewData = messagesData; + } else { + chartViewData = actionsData; } + putColorFromData(chartViewData, arrayList, themeDelegate); + } + } else { + for (int i = 0; i < 9; i++) { + ChartViewData chartViewData; + if (i == 0) { + chartViewData = growthData; + } else if (i == 1) { + chartViewData = followersData; + } else if (i == 2) { + chartViewData = interactionsData; + } else if (i == 3) { + chartViewData = ivInteractionsData; + } else if (i == 4) { + chartViewData = viewsBySourceData; + } else if (i == 5) { + chartViewData = newFollowersBySourceData; + } else if (i == 6) { + chartViewData = notificationsData; + } else if (i == 7) { + chartViewData = topHoursData; + } else { + chartViewData = languagesData; + } + putColorFromData(chartViewData, arrayList, themeDelegate); } } return arrayList; } - public static class OverviewData { + private void putColorFromData(ChartViewData chartViewData, ArrayList arrayList, ThemeDescription.ThemeDescriptionDelegate themeDelegate) { + if (chartViewData != null && chartViewData.chartData != null) { + for (ChartData.Line l : chartViewData.chartData.lines) { + if (l.colorKey != null) { + if (!Theme.hasThemeKey(l.colorKey)) { + Theme.setColor(l.colorKey, Theme.isCurrentThemeNight() ? l.colorDark : l.color, false); + Theme.setDefaultColor(l.colorKey, l.color); + } + arrayList.add(new ThemeDescription(null, 0, null, null, null, themeDelegate, l.colorKey)); + } + } + } + } + + public static class OverviewChannelData { String followersTitle; String followersPrimary; @@ -1735,7 +2162,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente String notificationsTitle; String notificationsPrimary; - public OverviewData(TLRPC.TL_stats_broadcastStats stats) { + public OverviewChannelData(TLRPC.TL_stats_broadcastStats stats) { int dif = (int) (stats.followers.current - stats.followers.previous); float difPercent = stats.followers.previous == 0 ? 0 : Math.abs(dif / (float) stats.followers.previous * 100f); followersTitle = LocaleController.getString("FollowersChartTitle", R.string.FollowersChartTitle); @@ -1787,6 +2214,80 @@ public class StatisticActivity extends BaseFragment implements NotificationCente } } + public static class OverviewChatData { + + String membersTitle; + String membersPrimary; + String membersSecondary; + boolean membersUp; + + String messagesTitle; + String messagesPrimary; + String messagesSecondary; + boolean messagesUp; + + String viewingMembersTitle; + String viewingMembersPrimary; + String viewingMembersSecondary; + boolean viewingMembersUp; + + String postingMembersTitle; + String postingMembersPrimary; + String postingMembersSecondary; + boolean postingMembersUp; + + public OverviewChatData(TLRPC.TL_stats_megagroupStats stats) { + int dif = (int) (stats.members.current - stats.members.previous); + float difPercent = stats.members.previous == 0 ? 0 : Math.abs(dif / (float) stats.members.previous * 100f); + membersTitle = LocaleController.getString("MembersOverviewTitle", R.string.MembersOverviewTitle); + membersPrimary = AndroidUtilities.formatWholeNumber((int) stats.members.current, 0); + + if (dif == 0 || difPercent == 0) { + membersSecondary = ""; + } else if (difPercent == (int) difPercent) { + membersSecondary = String.format(Locale.ENGLISH, "%s (%d%s)", (dif > 0 ? "+" : "") + AndroidUtilities.formatWholeNumber(dif, 0), (int) difPercent, "%"); + } else { + membersSecondary = String.format(Locale.ENGLISH, "%s (%.1f%s)", (dif > 0 ? "+" : "") + AndroidUtilities.formatWholeNumber(dif, 0), difPercent, "%"); + } + membersUp = dif >= 0; + + dif = (int) (stats.viewers.current - stats.viewers.previous); + difPercent = stats.viewers.previous == 0 ? 0 : Math.abs(dif / (float) stats.viewers.previous * 100f); + viewingMembersTitle = LocaleController.getString("ViewingMembers", R.string.ViewingMembers); + viewingMembersPrimary = AndroidUtilities.formatWholeNumber((int) stats.viewers.current, 0); + + if (dif == 0 || difPercent == 0) { + viewingMembersSecondary = ""; + } else { + viewingMembersSecondary = String.format(Locale.ENGLISH, "%s", (dif > 0 ? "+" : "") + AndroidUtilities.formatWholeNumber(dif, 0)); + } + viewingMembersUp = dif >= 0; + + + dif = (int) (stats.posters.current - stats.posters.previous); + difPercent = stats.posters.previous == 0 ? 0 : Math.abs(dif / (float) stats.posters.previous * 100f); + postingMembersTitle = LocaleController.getString("PostingMembers", R.string.PostingMembers); + postingMembersPrimary = AndroidUtilities.formatWholeNumber((int) stats.posters.current, 0); + if (dif == 0 || difPercent == 0) { + postingMembersSecondary = ""; + } else { + postingMembersSecondary = String.format(Locale.ENGLISH, "%s", (dif > 0 ? "+" : "") + AndroidUtilities.formatWholeNumber(dif, 0)); + } + postingMembersUp = dif >= 0; + + dif = (int) (stats.messages.current - stats.messages.previous); + difPercent = stats.messages.previous == 0 ? 0 : Math.abs(dif / (float) stats.messages.previous * 100f); + messagesTitle = LocaleController.getString("MessagesOverview", R.string.MessagesOverview); + messagesPrimary = AndroidUtilities.formatWholeNumber((int) stats.messages.current, 0); + if (dif == 0 || difPercent == 0) { + messagesSecondary = ""; + } else { + messagesSecondary = String.format(Locale.ENGLISH, "%s", (dif > 0 ? "+" : "") + AndroidUtilities.formatWholeNumber(dif, 0)); + } + messagesUp = dif >= 0; + } + } + public static class OverviewCell extends LinearLayout { TextView[] primary = new TextView[4]; @@ -1797,7 +2298,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente public OverviewCell(Context context) { super(context); setOrientation(VERTICAL); - setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), 0); + setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), AndroidUtilities.dp(16)); for (int i = 0; i < 2; i++) { LinearLayout linearLayout = new LinearLayout(context); linearLayout.setOrientation(HORIZONTAL); @@ -1826,11 +2327,11 @@ public class StatisticActivity extends BaseFragment implements NotificationCente contentCell.addView(title[i * 2 + j]); linearLayout.addView(contentCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 1f)); } - addView(linearLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 0, 0, 0, 0, 16)); + addView(linearLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, 0, 0, 0, 0, i == 0 ? 16 : 0)); } } - public void setData(OverviewData data) { + public void setData(OverviewChannelData data) { primary[0].setText(data.followersPrimary); primary[1].setText(data.notificationsPrimary); primary[2].setText(data.viewsPrimary); @@ -1840,7 +2341,7 @@ public class StatisticActivity extends BaseFragment implements NotificationCente secondary[0].setTag(data.followersUp ? Theme.key_windowBackgroundWhiteGreenText2 : Theme.key_windowBackgroundWhiteRedText5); secondary[1].setText(""); secondary[2].setText(data.viewsSecondary); - secondary[2].setTag(data.viewsUp ? Theme.key_windowBackgroundWhiteGreenText2: Theme.key_windowBackgroundWhiteRedText5); + secondary[2].setTag(data.viewsUp ? Theme.key_windowBackgroundWhiteGreenText2 : Theme.key_windowBackgroundWhiteRedText5); secondary[3].setText(data.sharesSecondary); secondary[3].setTag(data.sharesUp ? Theme.key_windowBackgroundWhiteGreenText2 : Theme.key_windowBackgroundWhiteRedText5); @@ -1852,6 +2353,31 @@ public class StatisticActivity extends BaseFragment implements NotificationCente updateColors(); } + public void setData(OverviewChatData data) { + primary[0].setText(data.membersPrimary); + primary[1].setText(data.messagesPrimary); + primary[2].setText(data.viewingMembersPrimary); + primary[3].setText(data.postingMembersPrimary); + + secondary[0].setText(data.membersSecondary); + secondary[0].setTag(data.membersUp ? Theme.key_windowBackgroundWhiteGreenText2 : Theme.key_windowBackgroundWhiteRedText5); + + secondary[1].setText(data.messagesSecondary); + secondary[1].setTag(data.messagesUp ? Theme.key_windowBackgroundWhiteGreenText2 : Theme.key_windowBackgroundWhiteRedText5); + + secondary[2].setText(data.viewingMembersSecondary); + secondary[2].setTag(data.viewingMembersUp ? Theme.key_windowBackgroundWhiteGreenText2 : Theme.key_windowBackgroundWhiteRedText5); + secondary[3].setText(data.postingMembersSecondary); + secondary[3].setTag(data.postingMembersUp ? Theme.key_windowBackgroundWhiteGreenText2 : Theme.key_windowBackgroundWhiteRedText5); + + title[0].setText(data.membersTitle); + title[1].setText(data.messagesTitle); + title[2].setText(data.viewingMembersTitle); + title[3].setText(data.postingMembersTitle); + + updateColors(); + } + private void updateColors() { for (int i = 0; i < 4; i++) { primary[i].setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); @@ -1865,4 +2391,230 @@ public class StatisticActivity extends BaseFragment implements NotificationCente } } + + public static class MemberData { + public TLRPC.User user; + int user_id; + public String description; + + public static MemberData from(TLRPC.TL_statsGroupTopPoster poster, ArrayList users) { + MemberData data = new MemberData(); + data.user_id = poster.user_id; + data.user = find(data.user_id, users); + StringBuilder stringBuilder = new StringBuilder(); + if (poster.messages > 0) { + stringBuilder.append(LocaleController.formatPluralString("messages", poster.messages)); + } + if (poster.avg_chars > 0) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(LocaleController.formatString("CharactersPerMessage", R.string.CharactersPerMessage, LocaleController.formatPluralString("Characters", poster.avg_chars))); + } + data.description = stringBuilder.toString(); + return data; + } + + public static MemberData from(TLRPC.TL_statsGroupTopAdmin admin, ArrayList users) { + MemberData data = new MemberData(); + data.user_id = admin.user_id; + data.user = find(data.user_id, users); + StringBuilder stringBuilder = new StringBuilder(); + if (admin.deleted > 0) { + stringBuilder.append(LocaleController.formatPluralString("Deletions", admin.deleted)); + } + if (admin.banned > 0) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(LocaleController.formatPluralString("Bans", admin.banned)); + } + if (admin.kicked > 0) { + if (stringBuilder.length() > 0) { + stringBuilder.append(", "); + } + stringBuilder.append(LocaleController.formatPluralString("Restrictions", admin.kicked)); + } + data.description = stringBuilder.toString(); + return data; + } + + public static MemberData from(TLRPC.TL_statsGroupTopInviter inviter, ArrayList users) { + MemberData data = new MemberData(); + data.user_id = inviter.user_id; + data.user = find(data.user_id, users); + if (inviter.invitations > 0) { + data.description = LocaleController.formatPluralString("Invitations", inviter.invitations); + } else { + data.description = ""; + } + return data; + } + + public static TLRPC.User find(int user_id, ArrayList users) { + for (TLRPC.User user : users) { + if (user.id == user_id) { + return user; + } + } + return null; + } + + public void onClick(BaseFragment fragment) { + Bundle bundle = new Bundle(); + bundle.putInt("user_id", user.id); + MessagesController.getInstance(UserConfig.selectedAccount).putUser(user, false); + fragment.presentFragment(new ProfileActivity(bundle)); + } + + public void onLongClick(TLRPC.ChatFull chat, StatisticActivity fragment, AlertDialog[] progressDialog) { + onLongClick(chat, fragment, progressDialog, true); + } + + private void onLongClick(TLRPC.ChatFull chat, StatisticActivity fragment, AlertDialog[] progressDialog, boolean userIsPracticant) { + MessagesController.getInstance(UserConfig.selectedAccount).putUser(user, false); + + final ArrayList items = new ArrayList<>(); + final ArrayList actions = new ArrayList<>(); + final ArrayList icons = new ArrayList<>(); + + TLRPC.TL_chatChannelParticipant currentParticipant = null; + TLRPC.TL_chatChannelParticipant currentUser = null; + + if (userIsPracticant && chat.participants.participants != null) { + int n = chat.participants.participants.size(); + for (int i = 0; i < n; i++) { + TLRPC.ChatParticipant participant = chat.participants.participants.get(i); + if (participant.user_id == user.id) { + if (participant instanceof TLRPC.TL_chatChannelParticipant) { + currentParticipant = (TLRPC.TL_chatChannelParticipant) participant; + } + } + if (participant.user_id == UserConfig.getInstance(UserConfig.selectedAccount).clientUserId) { + if (participant instanceof TLRPC.TL_chatChannelParticipant) { + currentUser = (TLRPC.TL_chatChannelParticipant) participant; + } + } + } + } + + items.add(LocaleController.getString("StatisticOpenProfile", R.string.StatisticOpenProfile)); + icons.add(R.drawable.menu_private); + actions.add(2); + items.add(LocaleController.getString("StatisticSearchUserHistory", R.string.StatisticSearchUserHistory)); + icons.add(R.drawable.menu_chats); + actions.add(1); + + if (userIsPracticant && currentParticipant == null) { + if (progressDialog[0] == null) { + progressDialog[0] = new AlertDialog(fragment.getFragmentView().getContext(), 3); + progressDialog[0].showDelayed(300); + } + TLRPC.TL_channels_getParticipant request = new TLRPC.TL_channels_getParticipant(); + request.channel = MessagesController.getInstance(UserConfig.selectedAccount).getInputChannel(chat.id); + request.user_id = MessagesController.getInstance(UserConfig.selectedAccount).getInputUser(user.id); + ConnectionsManager.getInstance(UserConfig.selectedAccount).sendRequest(request, (response, error) -> { + AndroidUtilities.runOnUIThread(() -> { + if (fragment.isFinishing() || fragment.getFragmentView() == null) { + return; + } + if (progressDialog[0] == null) { + return; + } + if (error == null) { + TLRPC.TL_channels_channelParticipant participant = (TLRPC.TL_channels_channelParticipant) response; + TLRPC.TL_chatChannelParticipant chatChannelParticipant = new TLRPC.TL_chatChannelParticipant(); + chatChannelParticipant.channelParticipant = participant.participant; + chatChannelParticipant.user_id = user.id; + chat.participants.participants.add(0, chatChannelParticipant); + onLongClick(chat, fragment, progressDialog); + } else { + onLongClick(chat, fragment, progressDialog, false); + } + }); + }); + return; + } + + if (userIsPracticant && currentUser == null) { + if (progressDialog[0] == null) { + progressDialog[0] = new AlertDialog(fragment.getFragmentView().getContext(), 3); + progressDialog[0].showDelayed(300); + } + TLRPC.TL_channels_getParticipant request = new TLRPC.TL_channels_getParticipant(); + request.channel = MessagesController.getInstance(UserConfig.selectedAccount).getInputChannel(chat.id); + request.user_id = MessagesController.getInstance(UserConfig.selectedAccount).getInputUser(UserConfig.getInstance(UserConfig.selectedAccount).clientUserId); + ConnectionsManager.getInstance(UserConfig.selectedAccount).sendRequest(request, (response, error) -> { + AndroidUtilities.runOnUIThread(() -> { + if (fragment.isFinishing() || fragment.getFragmentView() == null) { + return; + } + if (progressDialog[0] == null) { + return; + } + if (error == null) { + TLRPC.TL_channels_channelParticipant participant = (TLRPC.TL_channels_channelParticipant) response; + TLRPC.TL_chatChannelParticipant chatChannelParticipant = new TLRPC.TL_chatChannelParticipant(); + chatChannelParticipant.channelParticipant = participant.participant; + chatChannelParticipant.user_id = UserConfig.getInstance(UserConfig.selectedAccount).clientUserId; + chat.participants.participants.add(0, chatChannelParticipant); + onLongClick(chat, fragment, progressDialog); + } else { + onLongClick(chat, fragment, progressDialog, false); + } + }); + }); + return; + } + + if (progressDialog[0] != null) { + progressDialog[0].dismiss(); + progressDialog[0] = null; + } + + boolean isAdmin = false; + if (currentUser != null && currentParticipant != null && currentUser.channelParticipant.admin_rights != null && currentUser.channelParticipant.admin_rights.add_admins) { + isAdmin = currentParticipant.channelParticipant.admin_rights == null; + items.add(isAdmin ? LocaleController.getString("SetAsAdmin", R.string.SetAsAdmin) : LocaleController.getString("EditAdminRights", R.string.EditAdminRights)); + icons.add(isAdmin ? R.drawable.actions_addadmin : R.drawable.actions_permissions); + actions.add(0); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(fragment.getParentActivity()); + TLRPC.TL_chatChannelParticipant finalCurrentParticipant = currentParticipant; + boolean finalIsAdmin = isAdmin; + builder.setItems(items.toArray(new CharSequence[actions.size()]), AndroidUtilities.toIntArray(icons), (dialogInterface, i) -> { + if (actions.get(i) == 0) { + ChatRightsEditActivity newFragment = new ChatRightsEditActivity(user.id, chat.id, finalCurrentParticipant.channelParticipant.admin_rights, null, finalCurrentParticipant.channelParticipant.banned_rights, finalCurrentParticipant.channelParticipant.rank, ChatRightsEditActivity.TYPE_ADMIN, true, finalIsAdmin); + newFragment.setDelegate(new ChatRightsEditActivity.ChatRightsEditActivityDelegate() { + @Override + public void didSetRights(int rights, TLRPC.TL_chatAdminRights rightsAdmin, TLRPC.TL_chatBannedRights rightsBanned, String rank) { + if (rights == 0) { + finalCurrentParticipant.channelParticipant.admin_rights = null; + finalCurrentParticipant.channelParticipant.rank = ""; + } else { + finalCurrentParticipant.channelParticipant.admin_rights = rightsAdmin; + finalCurrentParticipant.channelParticipant.rank = rank; + } + } + + @Override + public void didChangeOwner(TLRPC.User user) { + + } + }); + fragment.presentFragment(newFragment); + } else if (actions.get(i) == 2) { + onClick(fragment); + } else { + Bundle bundle = new Bundle(); + bundle.putInt("chat_id", chat.id); + bundle.putInt("search_from_user_id", user.id); + fragment.presentFragment(new ChatActivity(bundle)); + } + }); + AlertDialog alertDialog = builder.create(); + fragment.showDialog(alertDialog); + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/TextSelectionHint.java b/TMessagesProj/src/main/java/org/telegram/ui/TextSelectionHint.java index f820e3152..aa89b0abb 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/TextSelectionHint.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/TextSelectionHint.java @@ -136,8 +136,13 @@ class TextSelectionHint extends View { } showOnMeasure = false; - lastW = getMeasuredHeight(); + lastW = getMeasuredWidth(); } + int h = textLayout.getHeight() + AndroidUtilities.dp(8) * 2; + if (h < AndroidUtilities.dp(56)) { + h = AndroidUtilities.dp(56); + } + setMeasuredDimension(getMeasuredWidth(), h); } Path path = new Path(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java index 216c75d1c..13affb03a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ThemeActivity.java @@ -41,6 +41,8 @@ import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; import android.widget.FrameLayout; import android.widget.TextView; @@ -239,12 +241,25 @@ public class ThemeActivity extends BaseFragment implements NotificationCenter.No @Override public void onSeekBarPressed(boolean pressed) { + } + @Override + public CharSequence getContentDescription() { + return String.valueOf(Math.round(startFontSize + (endFontSize - startFontSize) * sizeBar.getProgress())); + } + + @Override + public int getStepsCount() { + return endFontSize - startFontSize; } }); + sizeBar.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(sizeBar, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 38, Gravity.LEFT | Gravity.TOP, 5, 5, 39, 0)); messagesCell = new ThemePreviewMessagesCell(context, parentLayout, 0); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + messagesCell.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } addView(messagesCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 53, 0, 0)); } @@ -270,6 +285,17 @@ public class ThemeActivity extends BaseFragment implements NotificationCenter.No messagesCell.invalidate(); sizeBar.invalidate(); } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + sizeBar.getSeekBarAccessibilityDelegate().onInitializeAccessibilityNodeInfoInternal(this, info); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + return super.performAccessibilityAction(action, arguments) || sizeBar.getSeekBarAccessibilityDelegate().performAccessibilityActionInternal(this, action, arguments); + } } private class BubbleRadiusCell extends FrameLayout { @@ -298,9 +324,19 @@ public class ThemeActivity extends BaseFragment implements NotificationCenter.No @Override public void onSeekBarPressed(boolean pressed) { + } + @Override + public CharSequence getContentDescription() { + return String.valueOf(Math.round(startRadius + (endRadius - startRadius) * sizeBar.getProgress())); + } + + @Override + public int getStepsCount() { + return endRadius - startRadius; } }); + sizeBar.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); addView(sizeBar, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 38, Gravity.LEFT | Gravity.TOP, 5, 5, 39, 0)); } @@ -321,6 +357,17 @@ public class ThemeActivity extends BaseFragment implements NotificationCenter.No super.invalidate(); sizeBar.invalidate(); } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + sizeBar.getSeekBarAccessibilityDelegate().onInitializeAccessibilityNodeInfoInternal(this, info); + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + return super.performAccessibilityAction(action, arguments) || sizeBar.getSeekBarAccessibilityDelegate().performAccessibilityActionInternal(this, action, arguments); + } } public ThemeActivity(int type) { @@ -1196,6 +1243,7 @@ public class ThemeActivity extends BaseFragment implements NotificationCenter.No private float checkedState; private Theme.ThemeInfo currentTheme; private Theme.ThemeAccent currentAccent; + private boolean checked; InnerAccentView(Context context) { super(context); @@ -1208,7 +1256,7 @@ public class ThemeActivity extends BaseFragment implements NotificationCenter.No } void updateCheckedState(boolean animate) { - boolean checked = currentTheme.currentAccentId == currentAccent.id; + checked = currentTheme.currentAccentId == currentAccent.id; if (checkAnimator != null) { checkAnimator.cancel(); @@ -1275,8 +1323,17 @@ public class ThemeActivity extends BaseFragment implements NotificationCenter.No canvas.drawCircle(cx, cy, AndroidUtilities.dp(8) * (1.0f - checkedState), paint); } } - } + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setText(LocaleController.getString("ColorPickerMainColor", R.string.ColorPickerMainColor)); + info.setClassName(Button.class.getName()); + info.setChecked(checked); + info.setCheckable(true); + info.setEnabled(true); + } + } private static class InnerCustomAccentView extends View { private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -1326,6 +1383,14 @@ public class ThemeActivity extends BaseFragment implements NotificationCenter.No angle += Math.PI / 3; } } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setText(LocaleController.getString("ColorPickerMainColor", R.string.ColorPickerMainColor)); + info.setClassName(Button.class.getName()); + info.setEnabled(true); + } } private class ThemeAccentsListAdapter extends RecyclerListView.SelectionAdapter { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java index 4be4bb948..4330a9cc9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ThemePreviewActivity.java @@ -1893,7 +1893,7 @@ public class ThemePreviewActivity extends BaseFragment implements DownloadContro NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.didSetNewWallpapper); } if (screenType != SCREEN_TYPE_PREVIEW || accent != null) { - if (SharedConfig.getDevicePerfomanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW) { + if (SharedConfig.getDevicePerformanceClass() == SharedConfig.PERFORMANCE_CLASS_LOW) { int w = Math.min(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y); int h = Math.max(AndroidUtilities.displaySize.x, AndroidUtilities.displaySize.y); imageFilter = (int) (w / AndroidUtilities.density) + "_" + (int) (h / AndroidUtilities.density) + "_f"; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationSetupActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationSetupActivity.java index c20dc73f6..59eb0ce87 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationSetupActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationSetupActivity.java @@ -78,6 +78,7 @@ public class TwoStepVerificationSetupActivity extends BaseFragment { private TextView titleTextView; private TextView descriptionText; private TextView descriptionText2; + private TextView descriptionText3; private TextView topButton; private EditTextBoldCursor passwordEditText; private ScrollView scrollView; @@ -113,6 +114,19 @@ public class TwoStepVerificationSetupActivity extends BaseFragment { private RLottieDrawable[] animationDrawables; private Runnable setAnimationRunnable; + private Runnable finishCallback = () -> { + if (passwordEditText == null) { + return; + } + if (passwordEditText.length() != 0) { + animationDrawables[2].setCustomEndFrame(49); + animationDrawables[2].setProgress(0.0f, false); + imageView.playAnimation(); + } else { + setRandomMonkeyIdleAnimation(true); + } + }; + public static final int TYPE_ENTER_FIRST = 0; public static final int TYPE_ENTER_SECOND = 1; public static final int TYPE_ENTER_HINT = 2; @@ -798,11 +812,23 @@ public class TwoStepVerificationSetupActivity extends BaseFragment { }); FrameLayout frameLayout2 = new FrameLayout(context); - scrollViewLinearLayout.addView(frameLayout2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 36, 0, 33)); + scrollViewLinearLayout.addView(frameLayout2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 36, 0, 22)); frameLayout2.addView(buttonTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 42, Gravity.CENTER_HORIZONTAL | Gravity.TOP)); frameLayout2.addView(descriptionText2, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.TOP)); + if (currentType == TYPE_EMAIL_RECOVERY) { + descriptionText3 = new TextView(context); + descriptionText3.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteLinkText)); + descriptionText3.setGravity(Gravity.CENTER_HORIZONTAL); + descriptionText3.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + descriptionText3.setLineSpacing(AndroidUtilities.dp(2), 1); + descriptionText3.setPadding(AndroidUtilities.dp(32), 0, AndroidUtilities.dp(32), 0); + descriptionText3.setText(LocaleController.getString("RestoreEmailTroubleNoEmail", R.string.RestoreEmailTroubleNoEmail)); + scrollViewLinearLayout.addView(descriptionText3, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL | Gravity.TOP, 0, 0, 0, 25)); + descriptionText3.setOnClickListener(v -> showAlertWithText(LocaleController.getString("RestorePasswordNoEmailTitle", R.string.RestorePasswordNoEmailTitle), LocaleController.getString("RestoreEmailTroubleText", R.string.RestoreEmailTroubleText))); + } + fragmentView = container; actionBarBackground = new View(context) { @@ -876,15 +902,7 @@ public class TwoStepVerificationSetupActivity extends BaseFragment { animationDrawables[3] = new RLottieDrawable(R.raw.tsv_setup_monkey_peek, "" + R.raw.tsv_setup_monkey_peek, AndroidUtilities.dp(120), AndroidUtilities.dp(120), true, null); animationDrawables[4] = new RLottieDrawable(R.raw.tsv_setup_monkey_close_and_peek_to_idle, "" + R.raw.tsv_setup_monkey_close_and_peek_to_idle, AndroidUtilities.dp(120), AndroidUtilities.dp(120), true, null); animationDrawables[5] = new RLottieDrawable(R.raw.tsv_setup_monkey_close_and_peek, "" + R.raw.tsv_setup_monkey_close_and_peek, AndroidUtilities.dp(120), AndroidUtilities.dp(120), true, null); - animationDrawables[2].setOnFinishCallback(() -> { - if (passwordEditText.length() != 0) { - animationDrawables[2].setCustomEndFrame(49); - animationDrawables[2].setProgress(0.0f, false); - imageView.playAnimation(); - } else { - setRandomMonkeyIdleAnimation(true); - } - }, 97); + animationDrawables[2].setOnFinishCallback(finishCallback, 97); setRandomMonkeyIdleAnimation(true); break; } diff --git a/TMessagesProj/src/main/res/drawable-hdpi/menu_camera2.png b/TMessagesProj/src/main/res/drawable-hdpi/menu_camera2.png new file mode 100644 index 000000000..4c8c52b85 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/menu_camera2.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/minithumb_play.png b/TMessagesProj/src/main/res/drawable-hdpi/minithumb_play.png new file mode 100644 index 000000000..917963f69 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/minithumb_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/msg_addphoto.png b/TMessagesProj/src/main/res/drawable-hdpi/msg_addphoto.png new file mode 100644 index 000000000..26556cc89 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/msg_addphoto.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_arrow.png b/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_arrow.png new file mode 100644 index 000000000..a81a97a05 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_marker.png b/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_marker.png new file mode 100644 index 000000000..bf420efa5 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_marker.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_neon.png b/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_neon.png new file mode 100644 index 000000000..cf0df75ad Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_neon.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_pen.png b/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_pen.png new file mode 100644 index 000000000..578a42fe6 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/msg_draw_pen.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/msg_video.png b/TMessagesProj/src/main/res/drawable-hdpi/msg_video.png new file mode 100644 index 000000000..6d46e0197 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/msg_video.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/music_reverse.png b/TMessagesProj/src/main/res/drawable-hdpi/music_reverse.png deleted file mode 100755 index 0fb601c00..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/music_reverse.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/nocover.png b/TMessagesProj/src/main/res/drawable-hdpi/nocover.png deleted file mode 100755 index 3193227da..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/nocover.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/pl_next.png b/TMessagesProj/src/main/res/drawable-hdpi/pl_next.png deleted file mode 100755 index 3ac679fa1..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/pl_next.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/pl_pause.png b/TMessagesProj/src/main/res/drawable-hdpi/pl_pause.png deleted file mode 100755 index 7755a20ae..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/pl_pause.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/pl_play.png b/TMessagesProj/src/main/res/drawable-hdpi/pl_play.png deleted file mode 100755 index 1d6542850..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/pl_play.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/pl_previous.png b/TMessagesProj/src/main/res/drawable-hdpi/pl_previous.png deleted file mode 100755 index 5a625ea52..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/pl_previous.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/pl_repeat.png b/TMessagesProj/src/main/res/drawable-hdpi/pl_repeat.png deleted file mode 100644 index ca6f90d33..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/pl_repeat.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/pl_repeat1.png b/TMessagesProj/src/main/res/drawable-hdpi/pl_repeat1.png deleted file mode 100644 index 3874f9eeb..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/pl_repeat1.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/pl_shuffle.png b/TMessagesProj/src/main/res/drawable-hdpi/pl_shuffle.png deleted file mode 100755 index ffc262e2a..000000000 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/pl_shuffle.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_next.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_next.png new file mode 100644 index 000000000..60f7d57eb Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_next.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_order.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_order.png new file mode 100644 index 000000000..952bd6f77 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_order.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_pause.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_pause.png new file mode 100644 index 000000000..74e3f9a72 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_pause.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_play.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_play.png new file mode 100644 index 000000000..76ae59c06 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_previous.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_previous.png new file mode 100644 index 000000000..d6ec6cf6a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_previous.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeat_reverse.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeat_reverse.png new file mode 100644 index 000000000..5529c60e1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeat_reverse.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeat_shuffle.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeat_shuffle.png new file mode 100644 index 000000000..f0a6a79ba Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeat_shuffle.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeatall.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeatall.png new file mode 100644 index 000000000..c8b115bad Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeatall.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeatone.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeatone.png new file mode 100644 index 000000000..30cc4179b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_repeatone.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/player_new_shuffle.png b/TMessagesProj/src/main/res/drawable-hdpi/player_new_shuffle.png new file mode 100644 index 000000000..8760ea5c3 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/player_new_shuffle.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/video_quality1.png b/TMessagesProj/src/main/res/drawable-hdpi/video_quality1.png index 2c84840a6..3b02221e9 100644 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/video_quality1.png and b/TMessagesProj/src/main/res/drawable-hdpi/video_quality1.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/video_quality2.png b/TMessagesProj/src/main/res/drawable-hdpi/video_quality2.png index 60326c09c..72a7492e2 100644 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/video_quality2.png and b/TMessagesProj/src/main/res/drawable-hdpi/video_quality2.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/video_quality3.png b/TMessagesProj/src/main/res/drawable-hdpi/video_quality3.png index 1b3373a6f..75128357c 100644 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/video_quality3.png and b/TMessagesProj/src/main/res/drawable-hdpi/video_quality3.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/menu_camera2.png b/TMessagesProj/src/main/res/drawable-mdpi/menu_camera2.png new file mode 100644 index 000000000..bed2fd1de Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/menu_camera2.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/minithumb_play.png b/TMessagesProj/src/main/res/drawable-mdpi/minithumb_play.png new file mode 100644 index 000000000..6b9d14fab Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/minithumb_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/msg_addphoto.png b/TMessagesProj/src/main/res/drawable-mdpi/msg_addphoto.png new file mode 100644 index 000000000..056b50141 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/msg_addphoto.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_arrow.png b/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_arrow.png new file mode 100644 index 000000000..be8682a99 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_marker.png b/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_marker.png new file mode 100644 index 000000000..585da1972 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_marker.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_neon.png b/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_neon.png new file mode 100644 index 000000000..6febda2c1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_neon.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_pen.png b/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_pen.png new file mode 100644 index 000000000..6a92817d0 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/msg_draw_pen.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/msg_video.png b/TMessagesProj/src/main/res/drawable-mdpi/msg_video.png new file mode 100644 index 000000000..9de036654 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/msg_video.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/music_reverse.png b/TMessagesProj/src/main/res/drawable-mdpi/music_reverse.png deleted file mode 100755 index 03b7ddcf1..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/music_reverse.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/nocover.png b/TMessagesProj/src/main/res/drawable-mdpi/nocover.png deleted file mode 100755 index be516152e..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/nocover.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pl_next.png b/TMessagesProj/src/main/res/drawable-mdpi/pl_next.png deleted file mode 100755 index a14e23fdb..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/pl_next.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pl_pause.png b/TMessagesProj/src/main/res/drawable-mdpi/pl_pause.png deleted file mode 100755 index e88b9aa7f..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/pl_pause.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pl_play.png b/TMessagesProj/src/main/res/drawable-mdpi/pl_play.png deleted file mode 100755 index 8724b960e..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/pl_play.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pl_previous.png b/TMessagesProj/src/main/res/drawable-mdpi/pl_previous.png deleted file mode 100755 index 56dcd2555..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/pl_previous.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pl_repeat.png b/TMessagesProj/src/main/res/drawable-mdpi/pl_repeat.png deleted file mode 100644 index 49d8251ea..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/pl_repeat.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pl_repeat1.png b/TMessagesProj/src/main/res/drawable-mdpi/pl_repeat1.png deleted file mode 100644 index 105508574..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/pl_repeat1.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pl_shuffle.png b/TMessagesProj/src/main/res/drawable-mdpi/pl_shuffle.png deleted file mode 100755 index 5b79693e9..000000000 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/pl_shuffle.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_next.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_next.png new file mode 100644 index 000000000..13fa92c3f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_next.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_order.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_order.png new file mode 100644 index 000000000..9b66cb66e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_order.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_pause.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_pause.png new file mode 100644 index 000000000..00321760e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_pause.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_play.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_play.png new file mode 100644 index 000000000..9f6992b87 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_previous.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_previous.png new file mode 100644 index 000000000..47245319e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_previous.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeat_reverse.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeat_reverse.png new file mode 100644 index 000000000..28f9cd66f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeat_reverse.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeat_shuffle.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeat_shuffle.png new file mode 100644 index 000000000..b38f3c9e1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeat_shuffle.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeatall.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeatall.png new file mode 100644 index 000000000..0cac57375 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeatall.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeatone.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeatone.png new file mode 100644 index 000000000..e2b2a8162 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_repeatone.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/player_new_shuffle.png b/TMessagesProj/src/main/res/drawable-mdpi/player_new_shuffle.png new file mode 100644 index 000000000..0a753a301 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/player_new_shuffle.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/video_quality1.png b/TMessagesProj/src/main/res/drawable-mdpi/video_quality1.png index f3c028baa..9c283abf7 100644 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/video_quality1.png and b/TMessagesProj/src/main/res/drawable-mdpi/video_quality1.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/video_quality2.png b/TMessagesProj/src/main/res/drawable-mdpi/video_quality2.png index 5a221e41d..f3b942f7d 100644 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/video_quality2.png and b/TMessagesProj/src/main/res/drawable-mdpi/video_quality2.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/video_quality3.png b/TMessagesProj/src/main/res/drawable-mdpi/video_quality3.png index 73dd84808..4f99c242e 100644 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/video_quality3.png and b/TMessagesProj/src/main/res/drawable-mdpi/video_quality3.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/menu_camera2.png b/TMessagesProj/src/main/res/drawable-xhdpi/menu_camera2.png new file mode 100644 index 000000000..cc9810a81 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/menu_camera2.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/minithumb_play.png b/TMessagesProj/src/main/res/drawable-xhdpi/minithumb_play.png new file mode 100644 index 000000000..e5be4b4eb Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/minithumb_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/msg_addphoto.png b/TMessagesProj/src/main/res/drawable-xhdpi/msg_addphoto.png new file mode 100644 index 000000000..2afc570aa Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/msg_addphoto.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_arrow.png b/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_arrow.png new file mode 100644 index 000000000..9a6ee6d4d Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_marker.png b/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_marker.png new file mode 100644 index 000000000..71280bcea Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_marker.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_neon.png b/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_neon.png new file mode 100644 index 000000000..b6e7d3201 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_neon.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_pen.png b/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_pen.png new file mode 100644 index 000000000..c6a5e10c5 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/msg_draw_pen.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/msg_video.png b/TMessagesProj/src/main/res/drawable-xhdpi/msg_video.png new file mode 100644 index 000000000..95c97e5b3 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/msg_video.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/music_reverse.png b/TMessagesProj/src/main/res/drawable-xhdpi/music_reverse.png deleted file mode 100755 index 798f76da0..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/music_reverse.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/nocover.png b/TMessagesProj/src/main/res/drawable-xhdpi/nocover.png deleted file mode 100755 index 89545ec67..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/nocover.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pl_next.png b/TMessagesProj/src/main/res/drawable-xhdpi/pl_next.png deleted file mode 100755 index 36ce2ec33..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/pl_next.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pl_pause.png b/TMessagesProj/src/main/res/drawable-xhdpi/pl_pause.png deleted file mode 100755 index eafdf7752..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/pl_pause.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pl_play.png b/TMessagesProj/src/main/res/drawable-xhdpi/pl_play.png deleted file mode 100755 index f92f797ed..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/pl_play.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pl_previous.png b/TMessagesProj/src/main/res/drawable-xhdpi/pl_previous.png deleted file mode 100755 index f65c9d920..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/pl_previous.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pl_repeat.png b/TMessagesProj/src/main/res/drawable-xhdpi/pl_repeat.png deleted file mode 100644 index d999afa0e..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/pl_repeat.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pl_repeat1.png b/TMessagesProj/src/main/res/drawable-xhdpi/pl_repeat1.png deleted file mode 100644 index e6f8132df..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/pl_repeat1.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pl_shuffle.png b/TMessagesProj/src/main/res/drawable-xhdpi/pl_shuffle.png deleted file mode 100755 index 81a4e5bf4..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/pl_shuffle.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_next.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_next.png new file mode 100644 index 000000000..deb097fee Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_next.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_order.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_order.png new file mode 100644 index 000000000..00ebc60f1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_order.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_pause.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_pause.png new file mode 100644 index 000000000..cb2a690c1 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_pause.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_play.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_play.png new file mode 100644 index 000000000..46e62c08e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_previous.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_previous.png new file mode 100644 index 000000000..675d466f0 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_previous.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeat_reverse.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeat_reverse.png new file mode 100644 index 000000000..305e58ee0 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeat_reverse.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeat_shuffle.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeat_shuffle.png new file mode 100644 index 000000000..e4a62595b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeat_shuffle.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeatall.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeatall.png new file mode 100644 index 000000000..ec5ca0299 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeatall.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeatone.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeatone.png new file mode 100644 index 000000000..40f211255 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_repeatone.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/player_new_shuffle.png b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_shuffle.png new file mode 100644 index 000000000..d8659602a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/player_new_shuffle.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/video_quality1.png b/TMessagesProj/src/main/res/drawable-xhdpi/video_quality1.png index e06ec23ae..3070f5ea2 100644 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/video_quality1.png and b/TMessagesProj/src/main/res/drawable-xhdpi/video_quality1.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/video_quality2.png b/TMessagesProj/src/main/res/drawable-xhdpi/video_quality2.png index aa92c4fab..5b57524c6 100644 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/video_quality2.png and b/TMessagesProj/src/main/res/drawable-xhdpi/video_quality2.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/video_quality3.png b/TMessagesProj/src/main/res/drawable-xhdpi/video_quality3.png index 004e26b3f..b4c3f71bb 100644 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/video_quality3.png and b/TMessagesProj/src/main/res/drawable-xhdpi/video_quality3.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/menu_camera2.png b/TMessagesProj/src/main/res/drawable-xxhdpi/menu_camera2.png new file mode 100644 index 000000000..db39bdf73 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/menu_camera2.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/minithumb_play.png b/TMessagesProj/src/main/res/drawable-xxhdpi/minithumb_play.png new file mode 100644 index 000000000..6947e642a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/minithumb_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/msg_addphoto.png b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_addphoto.png new file mode 100644 index 000000000..f9edb8978 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_addphoto.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_arrow.png b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_arrow.png new file mode 100644 index 000000000..7cd0bdd30 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_arrow.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_marker.png b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_marker.png new file mode 100644 index 000000000..00f465683 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_marker.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_neon.png b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_neon.png new file mode 100644 index 000000000..69cd6be5e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_neon.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_pen.png b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_pen.png new file mode 100644 index 000000000..b7d2ac6b3 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_draw_pen.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/msg_video.png b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_video.png new file mode 100644 index 000000000..9d4b0e6de Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/msg_video.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/music_reverse.png b/TMessagesProj/src/main/res/drawable-xxhdpi/music_reverse.png deleted file mode 100755 index 2efb70c99..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/music_reverse.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/nocover.png b/TMessagesProj/src/main/res/drawable-xxhdpi/nocover.png deleted file mode 100755 index 37b656c51..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/nocover.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_next.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pl_next.png deleted file mode 100755 index 6cb192023..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_next.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_pause.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pl_pause.png deleted file mode 100755 index 730b4a5ab..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_pause.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_play.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pl_play.png deleted file mode 100755 index 6bf75867f..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_play.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_previous.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pl_previous.png deleted file mode 100755 index 941caed24..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_previous.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_repeat.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pl_repeat.png deleted file mode 100644 index c98c818c8..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_repeat.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_repeat1.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pl_repeat1.png deleted file mode 100644 index dee13fcc3..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_repeat1.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_shuffle.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pl_shuffle.png deleted file mode 100755 index 85fdec828..000000000 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/pl_shuffle.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_next.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_next.png new file mode 100644 index 000000000..92fe0b373 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_next.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_order.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_order.png new file mode 100644 index 000000000..491c22d33 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_order.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_pause.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_pause.png new file mode 100644 index 000000000..32d9a3a94 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_pause.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_play.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_play.png new file mode 100644 index 000000000..89ad19632 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_previous.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_previous.png new file mode 100644 index 000000000..3a68aad16 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_previous.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeat_reverse.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeat_reverse.png new file mode 100644 index 000000000..cd624774b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeat_reverse.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeat_shuffle.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeat_shuffle.png new file mode 100644 index 000000000..2fca09277 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeat_shuffle.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeatall.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeatall.png new file mode 100644 index 000000000..1e3160ff5 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeatall.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeatone.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeatone.png new file mode 100644 index 000000000..e551d2226 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_repeatone.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_shuffle.png b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_shuffle.png new file mode 100644 index 000000000..10e45b144 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/player_new_shuffle.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality1.png b/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality1.png index 347d36f78..8a79b40a4 100644 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality1.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality1.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality2.png b/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality2.png index 6596e8a65..8e4b8966b 100644 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality2.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality2.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality3.png b/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality3.png index 1f9354843..881be2001 100644 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality3.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/video_quality3.png differ diff --git a/TMessagesProj/src/main/res/drawable/paint_elliptical_preview.png b/TMessagesProj/src/main/res/drawable/paint_elliptical_preview.png deleted file mode 100644 index b0857ca17..000000000 Binary files a/TMessagesProj/src/main/res/drawable/paint_elliptical_preview.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable/paint_neon_preview.png b/TMessagesProj/src/main/res/drawable/paint_neon_preview.png deleted file mode 100644 index 54ec0f0ea..000000000 Binary files a/TMessagesProj/src/main/res/drawable/paint_neon_preview.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/drawable/paint_radial_preview.png b/TMessagesProj/src/main/res/drawable/paint_radial_preview.png deleted file mode 100644 index e57a53354..000000000 Binary files a/TMessagesProj/src/main/res/drawable/paint_radial_preview.png and /dev/null differ diff --git a/TMessagesProj/src/main/res/values-ar/strings.xml b/TMessagesProj/src/main/res/values-ar/strings.xml index 7efe1bf83..d6f8660d9 100644 --- a/TMessagesProj/src/main/res/values-ar/strings.xml +++ b/TMessagesProj/src/main/res/values-ar/strings.xml @@ -37,7 +37,7 @@ لم يصلك الرمز؟ إرسال الرمز برسالة SMS قصيرة إلغاء إعادة تعيين الحساب - شخص ما لديه صلاحية الوصول لرقمك **%1$s** طلب حذف حسابك وتعطيل التحقق بخطوتين من حسابك.\n\nإذا لم تكن أنت من قام بذلك فقم رجاء بإدخال الرمز الذي أرسلناه للتوِّ برسالة SMS قصيرة. + Somebody with access to **%1$s** has requested to delete your Telegram account and reset your 2-Step Verification password.\n\nIf this wasn\'t you, please enter the code we\'ve just sent you via SMS. You can also cancel this by *changing your phone number*. إعادة تعيين الحساب نظرًا لأن الحساب **%1$s** نشطٌ ومحميٌ بكلمة مرور؛ سنقوم بحذفه خلال أسبوع لدواعٍ أمنية.\n\nيمْكنك إلغاء هذه العملية في أي وقت. ستتمكن من إعادة تعيين حسابك خلال: @@ -223,6 +223,9 @@ تثبيت المحادثات يمكنك تثبيت عدد غير محدود من المحادثات المؤرشفة في الأعلى. اضغط هذا الزر باستمرار لجدولة رسالتك أو إرسالها دون صوت. + إخفاء المحادثات الجديدة؟ + أنت تتلقى الكثير من المحادثات الجديدة من مستخدمين ليسوا في قائمة جهات اتصالك، هل تريد أن تكون مثل هذه المحادثات **مكتومة تلقائيًا** و **مؤرشفة**؟ + اذهب إلى الإعدادات ترقية إلى مشرف تعديل صلاحيات المشرف @@ -326,6 +329,7 @@ البوتات الأعضاء الآخرون المشتركون الآخرون + انضم %1$s المستخدمون المقيدون المشرفون حذف القناة @@ -339,6 +343,7 @@ فضلًا قم باختيار رابط لقناتك العامة ليتمكن الناس من إيجادها عبر البحث ومشاركتِها مع غيرهم.\n\nإذا لم ترغب بذلك، ننصحك بإنشاء قناة خاصة بدلا منها. تم إنشاء القناة تم تغيير صورة القناة + تم تغيير المقطع المرئي للقناة تمت إزالة صورة القناة تم تغيير اسم القناة إلى un2 المعذرة، لقد قمت بحجز أسماء مستخدمين كثيرة. يمكنك تعطيل بعض روابط مجموعاتك وقنواتك القديمة, أو قم بإنشاء واحدة خاصة عوضًا عن ذلك. @@ -377,6 +382,7 @@ المعذرة، لا يمكنك إرسال رسائل لهذه القناة. "%1$s أضافك لقناة %2$s " تم تحديث صورة قناة %1$s + حدثت قناة %1$s مقطعها المرئي نشرت %1$s رسالة نشرت %1$s صورة نشرت %1$s مقطعًا مرئيًا @@ -467,6 +473,8 @@ إدارة المجموعة إدارة القناة سجل المحادثة للأعضاء الجدد + "تعيين صورة جديدة " + تعيين صورة أو مقطع مرئي ظاهر سيتمكن الأعضاء الجدد من مشاهدة الرسائل التي أُرسلت قبل انضمامهم. مخفي @@ -487,7 +495,10 @@ رابط انقر لإضافة رابط دائم اختر صورة + اختر صورة أو مقطعًا مرئيًّا + تعيين صورة جديدة التقاط صورة + "تسجيل مقطع مرئي " الاختيار من المعرض اختيار من المعرض البحث في الويب @@ -619,6 +630,8 @@ انضم un1 للقناة عيّن un1 صورة جديدة للمجموعة عيّن un1 صورة جديدة للقناة + عيّن un1 مقطعًا مرئيًّا للمجموعة + عيّن un1 مقطعًا مرئيًّا للقناة أزال un1 صورة المجموعة أزال un1 صورة القناة عدّل un1 هذه الرسالة: @@ -653,7 +666,7 @@ جعل un1 سجل المحادثة في المجموعة ظاهرًا للأعضاء الجدد جعل un1 سجل محادثة المجموعة مخفيًا للأعضاء الجدد un1 فعّل دعوات المجموعة - un1 عطّل دعوات المجموعة + عطّل un1 دعوات المجموعة فعّل un1 التواقيع عطّل un1 التواقيع غيَّر صلاحيات %1$s\n\nالمدة: %2$s @@ -702,10 +715,13 @@ مكتبتك الصوتية فارغة. لم يتم العثور على نتائج ما من مقاطع صوتية باسم **%1$s** في مكتبتك. + ما من شيء يطابق **%1$s**. مقطع صوتي المؤدي غير معروف عنوان غير معروف - عشوائي + تكرار الملف + تكرار القائمة + تشغيل عشوائي ترتيب عكسي اختر ملفًا @@ -909,7 +925,7 @@ حذف من عند %1$s حذف من عند كافة الأعضاء تم نسخ النص للحافظة - Hold to record audio. + اضغط مطولًا للتسجيل الصوتي. اضغط مطولًا لتسجيل الصوت، انقر للتحويل للمرئي. اضغط مطولًا لتسجيل مرئي، انقر للتحويل للصوتي. تجاهل الرسالة الصوتية @@ -989,6 +1005,15 @@ إخفاء المحادثة هل أنت متأكد أنك تريد إخفاء هذه المحادثة؟ إخفاء + أرسل رسالة أو اضغط على ملصق التحية أدناه لإرساله وإظهار أنك مستعد للتواصل. + %1$s على بعد %2$s + %1$s على بعد %2$s + تم نقل المحادثة إلى القائمة الرئيسية. + ي + ث + د + س + أس عيّن %1$s عداد التدمير الذاتي ليصبح %2$s قمت بتعيين عداد التدمير الذاتي ليصبح %1$s @@ -1043,6 +1068,7 @@ أضافك %1$s لمجموعة %2$s انضم %1$s للمجموعة %2$s غيّرَ %1$s صورة مجموعة %2$s + غيّر %1$s المقطع المرئي للمجموعة %2$s %1$s أضاف %3$s للمجموعة %2$s عاد %1$s لمجموعة %2$s انضم %1$s لمجموعة %2$s @@ -1147,6 +1173,11 @@ حتى %1$s المعذرة، هذه المجموعة ممتلئة. المعذرة، هذه المحادثة غير موجودة. + هذه القناة خاصة. يرجى الانضمام إليها لتتمكن من عرض محتواها. + هذه المجموعة خاصة. يرجى الانضمام إليها للاستمرار في عرض محتواها. + انضمام إلى القناة + انضمام إلى المجموعة + انضمام تم نسخ الرابط للحافظة تم نسخ الرابط للحافظة.\nلن يعمل هذا الرابط إلا لأعضاء هذه المحادثة. للأسف لا يمكنك الوصول إلى هذه الرسالة؛ أنت لست عضوًا في المحادثة التي تم نشرها فيها. @@ -1670,7 +1701,7 @@ تشغيل الوسائط تلقائيًا الرفع للتحدث حفظ في المعرض - Sound muted + تم كتم الصوت تعديل الاسم تخصيص مخصص @@ -1684,7 +1715,7 @@ مطلقًا إعادة الإشعارات يمكنك تغيير رقم تيليجرام الخاص بك هنا، سيتم نقل حسابك وجميع بياناتك التي في سحابة تيليجرام بما فيها رسائلك، الوسائط، جهات الاتصال وغيرها للرقم الجديد. - سيحصل المستخدمون على رقمك الجديد في حال وجوده في دليل عناوينهم أو في حال كانت إعدادات خصوصيتك تسمح لهم برؤيته. يمكنك تغيير ذلك في الإعدادات > الخصوصية والأمان > رقم الهاتف. + سيحصل المستخدمون على رقمك الجديد في حال وجوده في جهات اتصالهم أو في حال كانت إعدادات خصوصيتك تسمح لهم برؤيته. يمكنك تغيير ذلك في الإعدادات > الخصوصية والأمان > رقم الهاتف. تغيير الرقم تغيير الرقم الرقم الجديد @@ -1761,7 +1792,7 @@ بروكسي MTProto لم يتم إعداد البروكسي الذي تستخدمه بشكل صحيح وسيتم تعطيله. يرجى العثور على واحد آخر. راعي البروكسي - يقوم البروكسي الخاص بك بعرض هذه القناة. لإزالتها من قائمة محادثاتك، قم بتعطيل البروكسي من إعدادات تيليجرام. + تُعرض هذه القناة من قبل البروكسي. لإزالة هذه القناة من قائمة محادثاتك؛ عطّل اتصال البروكسي من إعدادات تيليجرام. إعدادات بروكسي SOCKS5 إعدادات بروكسي MTProto. قد يعرض هذا البروكسي قناة إعلانية في قائمة محادثاتك.\nلن يكشف هذا عن أي من بيانات تيليجرام الخاصة بك. @@ -1786,7 +1817,7 @@ الملصقات والأقنعة اختيار لون مخصص للتطبيق إظهار الإشعارات من - جميع الحسابات + كل الحسابات عطّل هذا الخيار إذا كنت تريد استلام الإشعارات فقط من الحساب الذي تستخدمه حاليًا. إشعارات المحادثات المحادثات الخاصة @@ -1834,6 +1865,7 @@ امسح رمز الـ QR لربط حسابك. يمكن استخدام هذا الرمز للسماح لشخص ما بتسجيل الدخول إلى حسابك في تيليجرام.\n\nلتأكيد عملية تسجيل الدخول؛ يرجى الذهاب إلى الإعدادات > الأجهزة > مسح QR وامسح الرمز. يحتاج تيليجرام الوصول إلى الكاميرا لتتمكن من مسح رمز QR. + "تعيين صورة للملف الشخصي " قاعدة البيانات على الجهاز مسح قاعدة البيانات المحلية @@ -2081,7 +2113,7 @@ إنشاء مجلد جديد أنشئ مجلدات لمختلف المجموعات والمحادثات وتنقّل بينها بسرعة. المجلدات المقترحة - جميع المحادثات + كل المحادثات الكل إعادة ترتيب تعديل المجلد @@ -2226,6 +2258,10 @@ على بُعد %1$s ك.م على بُعد %1$s قدم على بُعد %1$s ميل + يبعد %1$s م عنك + يبعد %1$s كم عنك + يبعد %1$s ق عنك + يبعد %1$s ميل عنك الاتجاهات لم يتم العثور على أي مكان ما من أماكن باسم **%1$s** بجانبك. @@ -2328,6 +2364,7 @@ حبوب زيادة الحدة التلاشي + مظهر أنعم الظلال الإضاءات الكل @@ -2343,7 +2380,11 @@ هل ترغب حقًا في حذف هذا المقطع المرئي؟ حذف الصورة المتحركة هل ترغب حقًا في حذف هذه الصورة المتحركة؟ + هل تريد بالتأكيد حذف هذه الصورة من عند الجميع؟ + هل تريد بالتأكيد حذف هذا المقطع المرئي من عند الجميع؟ + هل تريد بالتأكيد حذف هذه الصورة المتحركة من عند الجميع؟ تجاهل التغييرات؟ + هل ترغب حقًا في تجاهل جميع التغييرات؟ مسح سجل البحث؟ هل ترغب حقًا في مسح سجل البحث؟ مسح سجل البحث @@ -2359,6 +2400,10 @@ الوصف حذف تعديل + قلم + قلم التحديد + مضيئ + سهم تكرار حدود عادي @@ -2370,19 +2415,28 @@ تجميع الوسائط في رسالة واحدة إرسال دون تجميع إرسال دون ضغط + اختر غلافا للمقطع المرئي في ملفك الشخصي + تعيين كأساسي + فتح في المحرر + صورة العرض الأساسية لملفك الشخصي. + المقطع المرئي الأساسي لملفك الشخصي. + صورة العرض الأساسية للقناة. + المقطع المرئي الأساسي للقناة. + صورة العرض الأساسية للمجموعة. + المقطع المرئي الأساسي للمجموعة. التحقق بخطوتين التحقق بخطوتين تم تعيين كلمة المرور! سيتم طلب كلمة المرور هذه عندما تسجل الدخول في جهاز جديد بالإضافة إلى الرمز الذي نرسله برسالة نصية SMS. العودة للإعدادات - Return to Passport + العودة إلى جواز السفر تعيين كلمة المرور تعيين كلمة مرور إضافية يمكنك تعيين كلمة مرور إضافية يتم طلبها عند تسجيل الدخول من جهاز غير معروف، إضافةً إلى الرمز الذي يصلك في رسالة SMS قصيرة. كلمة مرورك - On - Off + مفعّل + معطل قم بإدخال كلمة مرورك يرجى إدخال كلمة مرورك لإتمام عملية النقل. قم بإدخال كلمة مرور @@ -2404,7 +2458,7 @@ تم تفعيل كلمة المرور الخاصة بالتحقق بخطوتين. تم تغيير كلمة مرور التحقق بخطوتين. تم تفعيل بريد الاسترداد الإلكتروني لعملية التحقق بخطوتين. - Your recovery email for Two-Step Verification is changed. + تم تغيير بريدك الإلكتروني لاستراد رمز التحقق بخطوتين. تغيير كلمة المرور إيقاف كلمة المرور تعيين بريد استرداد @@ -2435,6 +2489,7 @@ لقد قمنا بإرسال رمز الاسترداد إلى بريدك الإلكتروني الذي اخترته مسبقًا:\n\n%1$s يرجى تفقُّدُ بريدك وإدخال الرمز المرسَل إليك والمكون من 6 أرقام. هل تواجه صعوبة في الوصول إلى بريدك %1$s؟ + هل تواجه مشكلة في الدخول إلى بريدك الإلكتروني؟ إذا لم تتمكن من الوصول إلى بريدك الإلكتروني فإن خياراتك المتبقية هي إما أن تتذكر كلمةَ مرورك، أو أن تعيد تعيين حسابك وتفقدَ جميعَ محتوياته. إعادة تعيين حسابي إذا قمت بإعادة تعيين حسابك، ستفقد كافّة محادثاتك ورسائلك، بالإضافة إلى الوسائط والملفات التي تمت مشاركتها. @@ -2463,8 +2518,8 @@ الرسائل والبيانات الأخرى المرسلة الواردة - البايتات المرسلة - البايتات الواردة + البيانات المرسلة + البيانات المستلمة الملفات إعدادات المكالمات المكالمات صادرة @@ -2527,8 +2582,12 @@ لا أحد (+%1$d) الأمان التدمير الذاتي للحساب - إعدادات متقدمة + "المحادثات الجديدة من مستخدمين مجهولين " + أرشفة وكتم + أرشفة تلقائية وكتم للمحادثات والمجموعات والقنوات الجديدة من غير جهات الاتصال. + حذف حسابي حذف حسابي إن غِبتُ لمدة + إن تغيبت لمدة إذا لم تستخدم حسابَ تيليجرام هذا لمرة واحدة على الأقل خلالَ هذه المدة فسيتم حذفه نهائيًا مع جميع محادثاتك وجهات اتصالك. من يمْكنه رؤية آخر ظهور لك؟ إضافة استثناءات @@ -2627,6 +2686,7 @@ un1 أضاف un2 أزال un1 صورة المجموعة غيّرَ un1 صورة المجموعة + غيّر un1 المقطع المرئي للمجموعة غيّرَ un1 اسم المجموعة ليصبح un2 انشأ un1 المجموعة لقد أزلت un2 @@ -2638,6 +2698,7 @@ un1 أحرز %1$s في un2 لقد أزلتَ صورة المجموعة لقد غيّرتَ صورة المجموعة + غيّرت المقطع المرئي للمجموعة لقد غيّرتَ اسم المجموعة ليصبح un2 لقد أنشأت المجموعة un1 أزالك @@ -2848,6 +2909,52 @@ %s قاعدة بيانات تيليجرام المحلية تم تحرير %s من جهازك! %s بيانات أخرى + "أعضاء المجموعة " + الأعضاء الجدد حسب المصدر + اللغة الأساسية للأعضاء + الرسائل + الأحداث + الأعضاء + الرسائل + يشاهدون الرسائل + " ينشرون الرسائل" + %1$d أحرف + حرفًا واحدًا + حرفان + %1$d أحرف + %1$d حرفًا + %1$d حرف + حذف %1$d + حذف %1$d + حذف %1$d + حذف %1$d + حذف %1$d + حذف %1$d + حظر %1$d + حظر %1$d + حظر %1$d + حظر %1$d + حظر %1$d + حظر %1$d + %1$d تقييدات + تقييد واحد + تقييدان + %1$d تقييدات + %1$d تقييدًا + %1$d تقييد + %1$d إضافات + إضافة واحدة + إضافتان + %1$d إضافات + %1$d إضافة + %1$d إضافة + المشرفون النشطون + الأعضاء النشطون + أشهر المضيفين + %s للرسالة + أنشط أيام الأسبوع + "يقرؤون الرسائل " + فتح الملف الشخصي تيليجرام سريع @@ -3392,6 +3499,7 @@ محادثة سرية تم الإرسال %s تم التسليم %s + مجدولة لـ %s العودة للخلف فتح قائمة التنقل %2$s من قِبل %1$s @@ -3448,6 +3556,7 @@ تصغير اللوحة خيارات المستخدم تدوير + عكس محرر الصور تعديلات عارض الصور @@ -3462,6 +3571,8 @@ أخذ صورة أخرى تم قرائتها غير مقروءة + يتم الإرسال + مشكلة في الإرسال لم تُسمع بعد نتيجة البحث التالية نتيجة البحث السابقة @@ -3472,6 +3583,36 @@ افلت لرؤية الأرشيف حجم أصغر دقة أعلى + %1$s من %2$s + تم تنزيل %1$s من %2$s + تم رفع %1$s من %2$s + شوهدت %1$d مرّة + شوهدت %1$d مرّة + شوهدت %1$d مرات + شوهدت %1$d مرّة + شوهدت %1$d مرّة + Send %1$d photo + Send %1$d photos + Send %1$d photos + Send %1$d photos + Send %1$d photos + Send %1$d file + Send %1$d files + Send %1$d files + Send %1$d files + Send %1$d files + Send %1$d audio file + Send %1$d audio files + Send %1$d audio files + Send %1$d audio files + Send %1$d audio files + Share in %1$d chat + Share in %1$d chats + Share in %1$d chats + Share in %1$d chats + Share in %1$d chats + Correct answer + Picture-in-Picture mode MMM dd yyyy, h:mm a MMM dd yyyy, HH:mm diff --git a/TMessagesProj/src/main/res/values-de/strings.xml b/TMessagesProj/src/main/res/values-de/strings.xml index 2d84ac43b..fa06bd9dc 100644 --- a/TMessagesProj/src/main/res/values-de/strings.xml +++ b/TMessagesProj/src/main/res/values-de/strings.xml @@ -9,7 +9,7 @@ Weiter auf Deutsch Dein Telefon - Bitte bestätige deine Landesvorwahl und deine Telefonnummer. + Bitte bestätige deine Landeskennzahl und deine Telefonnummer. Wähle ein Land Ungültige Landeskennzahl Dieses Konto ist bereits in der App angemeldet. @@ -37,7 +37,7 @@ Code nicht erhalten? Code per SMS senden Zurücksetzung abbrechen - Jemand mit Zugang zu deiner Telefonnummer **%1$s** hat die Kontolöschung und Zurücksetzung der zweistufigen Bestätigung beantragt. Wenn du das nicht selbst gewesen bist, tippe den Code der SMS ein, den wir dir gerade gesendet haben. + Jemand mit Zugang zu deiner Telefonnummer **%1$s** hat die Kontolöschung und Zurücksetzung der zweistufige Bestätigung beantragt.\n\nWenn du das nicht selbst gewesen bist, tippe den Code der SMS ein, den wir dir gerade gesendet haben oder **ändere deine Telefonnummer**. Konto zurücksetzen Da dein Konto **%1$s** aktiv und durch ein Passwort geschützt ist, löschen wir es aus Sicherheitsgründen in einer Woche.\n\nDu kannst den Vorgang jederzeit abbrechen. Du kannst dein Konto zurücksetzen in: @@ -223,6 +223,9 @@ Angeheftete Chats Unbegrenzt viele archivierte Chats\nkannst du oben anheften. Halte diese Knopf, um deine Nachricht zu planen oder ohne Ton zu senden. + Neue Chats verstecken? + Du erhältst viele Chats von Leuten, die du nicht in deinen Kontakten hast. Möchtest du solche Chats zukünftig **automatisch stummschalten** und **archivieren**? + ZU EINSTELLUNGEN Zum Admin machen Adminrechte bearbeiten @@ -326,6 +329,7 @@ Bots Weitere Mitglieder Weitere Abonnenten + beigetreten: %1$s Eingeschränke Nutzer Administratoren Kanal löschen @@ -339,6 +343,7 @@ Bitte wähle einen Link für deinen öffentlichen Kanal, damit andere ihn finden und weiter verbreiten können.\n\nWenn du das nicht möchtest, empfehlen wir dir einen privaten Kanal. Kanal erstellt Bild geändert + Kanalvideo geändert Bild gelöscht Kanalname zu un2 geändert Du hast leider zu viele öffentliche Benutzernamen erstellt. Du kannst jederzeit den Link einer älteren Gruppe oder eines Kanals entfernen. @@ -377,6 +382,7 @@ Du darfst in diesem Kanal nichts schreiben. %1$s hat dich dem Kanal %2$s hinzugefügt Kanal %1$s hat das Bild geändert + Kanal %1$s hat das Video aktualisiert %1$s hat eine Nachricht gesendet %1$s hat ein Bild gesendet %1$s hat ein Video gesendet @@ -467,6 +473,8 @@ Gruppe verwalten Kanal verwalten Chatverlauf für neue Mitglieder + Neues Bild festlegen + Bild oder Video festlegen Sichtbar Neue Mitglieder sehen Nachrichten, die vor ihrem Beitritt gesendet wurden. Versteckt @@ -487,7 +495,10 @@ Link Für dauerhaften Link antippen Bild auswählen + Bild oder Video festlegen + Neues Bild festlegen Foto aufnehmen + Video aufnehmen Aus der Galerie Aus der Galerie wählen Aus dem Web @@ -619,6 +630,8 @@ un1 ist dem Kanal beigetreten un1 hat das Gruppenbild geändert un1 hat das Kanalbild geändert + un1 hat ein neues Gruppenvideo festgelegt + un1 hat ein neues Kanalvideo festgelegt un1 hat das Gruppenbild entfernt un1 hat das Kanalbild entfernt un1 hat diese Nachricht bearbeitet: @@ -702,10 +715,13 @@ Deine Musikbibliothek ist leer Keine Ergebnisse gefunden **%1$s** wurde nicht in deiner Musikbibliothek gefunden. + Keine Treffer für **%1$s**. Musik Unbekannter Künstler Unbekannter Titel - Zufallswiedergabe + Titel wiederholen + Liste wiederholen + Liste mischen Reihenfolge umkehren Datei auswählen @@ -812,7 +828,7 @@ Sprachdatei benutzen Thema anwenden Nicht unterstützte Datei - Selbstzerstörungs-Timer setzen + Selbstzerstörungs-Timer Servicemeldungen Lade Linkvorschau... ÖFFNEN MIT... @@ -909,7 +925,7 @@ Bei %1$s löschen Bei allen Mitgliedern löschen Text in die Zwischenablage kopiert - Hold to record audio. + Halten, um Audio aufzunehmen. Halten für Audioaufnahme. Tippen für Video. Halten für Videoaufnahme. Tippen für Audio. Sprachnachricht verwerfen @@ -989,9 +1005,18 @@ Chat verstecken Wirklich diesen Chat verstecken? Verstecken + Sende eine Nachricht oder tippe auf die Begrüssung unten. + %1$s ist %2$s + %1$s ist %2$s + Chat wurde in die Chatliste verschoben. + t + s + m + h + w - %1$s hat den Selbstzerstörungs-Timer auf %2$s gesetzt - Du hast den Selbstzerstörungs-Timer auf %1$s gesetzt + %1$s hat den Timer auf %2$s gesetzt + Du hast den Timer auf %1$s gesetzt %1$s hat den Selbstzerstörungs-Timer deaktiviert Du hast den Selbstzerstörungs-Timer deaktiviert Du hast eine neue Nachricht @@ -1043,6 +1068,7 @@ %1$s hat dich in die Gruppe %2$s eingeladen %1$s hat die Gruppe %2$s umbenannt %1$s hat das Gruppenbild von %2$s geändert + %1$s hat das Gruppenvideo bei %2$s geändert %1$s hat %3$s in die Gruppe %2$s eingeladen %1$s ist in die Gruppe %2$s zurückgekehrt %1$s ist der Gruppe %2$s beigetreten @@ -1147,6 +1173,11 @@ bis zu %1$s Leider ist diese Gruppe schon voll. Leider gibt es diesen Chat nicht. + Dieser Kanal ist privat. Jetzt beitreten, um den Inhalt weiterhin anzusehen. + Diese Gruppe ist privat. Beitreten, um den Inhalt zu sehen. + Kanal beitreten + Gruppe beitreten + BEITRETEN Link in die Zwischenablage kopiert Link in Zwischenablage kopiert.\nDer Link funktioniert nur für Mitglieder dieses Chats. Leider kannst du auf diese Nachricht nicht zugreifen. Du bist kein Mitglied des Chats, in dem sie veröffentlicht wurde. @@ -1642,7 +1673,7 @@ Auto-Download (Videos und GIFs) Maximale Videogrösse Maximale Dateigröße - Nächstes Lied vorladen + Nächsten Titel vorladen Nächsten Titel während der Wiedergabe herunterladen. Größere Videos vorladen Die ersten Sekunden (1-2 MB) von Videos vorladen, die größer als %1$s sind. @@ -1834,6 +1865,7 @@ Scanne den QR-Code mit deinem Handy, um dich mit deinem Konto zu verbinden. Mit diesem Code kann man sich bei deinem Telegram Konto anmelden.\n\nUm die Anmeldung zu bestätigen, in die Einstellungen von Telegram wechseln > Geräte > QR-Code scannen und den Code mit der Kamera scannen. Telegram benötigt zum Scannen von QR-Codes Zugriff auf deine Kamera. + Profilbild festlegen Lokale Datenbank Lokale Datenbank leeren @@ -2226,6 +2258,10 @@ %1$s km entfernt %1$s ft entfernt %1$s mi entfernt + %1$sm entfernt + %1$skm entfernt + %1$sft entfernt + %1$smi entfernt Wegbeschreibung Keine Orte gefunden Leider gibt es keine Plätze mit dem\nNamen **%1$s** in deiner Nähe. @@ -2328,6 +2364,7 @@ Körnung Schärfe Verblassen + Haut glätten TIEFEN LICHTER ALLE @@ -2343,7 +2380,11 @@ Möchtest du wirklich dieses Video löschen? GIF löschen Sicher, dass du dieses GIF wirklich löschen willst? + Möchtest du wirklich dieses Bild bei allen löschen? + Möchtest du das Video wirklich bei allen löschen? + "Möchtest du das GIF wirklich bei allen löschen? " Änderungen verwerfen? + Möchtest du wirklich alle Änderungen verwerfen? Suchverlauf löschen? Wirklich den Suchverlauf leeren? Suchverlauf leeren @@ -2359,6 +2400,10 @@ Beschriftung Löschen Bearbeiten + Stift + Marker + Neon + Pfeil Klonen Kontur Normal @@ -2370,6 +2415,15 @@ Zeigt alle Medien in einer Nachricht Ohne Gruppierung senden Ohne Komprimierung senden + Wähle ein Titelbild für das Profilvideo + Als Standard festlegen + Im Editor öffnen + Das ist jetzt dein Hauptprofilbild. + Das ist jetzt dein Hauptprofilvideo. + Das ist jetzt das Hauptbild des Kanals. + Das ist jetzt das Hauptvideo des Kanals. + Das ist jetzt das Hauptbild der Gruppe. + Das ist jetzt das Hauptvideo der Gruppe. Zweistufige Bestätigung Zweistufige Bestätigung @@ -2404,7 +2458,7 @@ Dein Passwort für die zweistufige Bestätigung ist jetzt aktiv. Dein Passwort für die zweistufige Bestätigung wurde geändert. Deine Wiederherstellungs-E-Mail für die zweistufige Bestätigung ist jetzt aktiv. - Your recovery email for Two-Step Verification is changed. + Deine Wiederherstellungs-E-Mail für die zweistufige Bestätigung wurde geändert. Passwort ändern Passwort deaktivieren E-Mail festlegen @@ -2435,6 +2489,7 @@ Wir haben den Wiederherstellungscode an diese Adresse geschickt:\n\n%1$s Überprüfe deine Mails und gib den 6-stelligen Code aus unserer E-Mail ein. Du hast keinen Zugang zu deiner Adresse %1$s? + Kein Zugang zu deiner E-Mail-Adresse? Wenn du nicht in deine E-Mails kommst, kannst du nur hoffen, dass dir dein Passwort wieder einfällt oder du musst dein Telegram-Konto zurücksetzen. KONTO ZURÜCKSETZEN Wenn du dein Konto zurücksetzt, verlierst du alle Chats und Nachrichten, ebenso deine geteilten Bilder und Videos. @@ -2527,8 +2582,12 @@ Niemand (+%1$d) Sicherheit Automatische Kontolöschung - Erweitert + Neue Chats von Unbekannten + Archivieren und Stumm + Diese Option archiviert und schaltet neue Chats, Gruppen und Kanäle von Nicht-Kontakten stumm bei dir. + Mein Konto löschen Konto löschen bei Inaktivität + Wenn inaktiv für Wenn du innerhalb dieser Zeit nicht online bist, wird dein Konto mit allen Nachrichten und Kontakten gelöscht. Wer darf deinen Online-Status sehen? Ausnahmen hinzufügen @@ -2627,6 +2686,7 @@ un1 hat un2 hinzugefügt un1 hat das Gruppenbild entfernt un1 hat das Gruppenbild geändert + un1 hat das Gruppenvideo geändert un1 hat den Namen der Gruppe in un2 geändert un1 hat die Gruppe erstellt Du hast un2 aus der Gruppe entfernt @@ -2638,6 +2698,7 @@ un1 hat %1$s bei un2 erzielt Du hast das Gruppenbild entfernt Du hast das Gruppenbild geändert + Du hast das Gruppenvideo geändert Du hast den Gruppennamen in un2 geändert Du hast die Gruppe erstellt un1 hat dich aus der Gruppe entfernt @@ -2681,7 +2742,7 @@ un1 hat ein Bildschirmfoto gemacht! Limit erreicht - Inactive Chats + Inaktive Chats Kanal, inaktiv seit %1$s %1$s, inaktiv seit %2$s %1$s verlassen @@ -2848,6 +2909,52 @@ %s Telegrams lokale Datenbank %s auf deinem Gerät freigegeben! %s Sonstige Daten + Gruppenmitglieder + Neue Mitglieder nach Herkunft + Hauptsprache der Mitglieder + Nachrichten + Aktionen + Mitglieder + Nachrichten + Leser + Versender + %1$d Zeichen + %1$d Zeichen + %1$d Zeichen + %1$d Zeichen + %1$d Zeichen + %1$d Zeichen + %1$d Löschungen + %1$d Löschung + %1$d Löschungen + %1$d Löschungen + %1$d Löschungen + %1$d Löschungen + %1$d Sperren + %1$d Sperre + %1$d Sperren + %1$d Sperren + %1$d Sperren + %1$d Sperren + %1$d Einschränkungen + %1$d Einschränkung + %1$d Einschränkungen + %1$d Einschränkungen + %1$d Einschränkungen + %1$d Einschränkungen + %1$d Einladungen + %1$d Einladung + %1$d Einladungen + %1$d Einladungen + %1$d Einladungen + %1$d Einladungen + Aktivste Admins + Aktivste Mitglieder + Aktivste Einladende + %s pro Nachricht + Aktivste Wochentage + Nachrichten anzeigen + Profil öffnen Telegram Schnell @@ -3392,6 +3499,7 @@ Geheimer Chat Gesendet %s Empfangen %s + Geplant am %s Zurückgehen Navigationsmenü öffnen %2$s von %1$s @@ -3448,6 +3556,7 @@ Panel einklappen Benutzeroptionen Drehen + Spiegeln Bildbearbeitung Anpassungen Bildbetrachter @@ -3462,6 +3571,8 @@ Nimm ein weiteres Bild auf Gesehen Noch nicht gesehen + Senden + Sendefehler Noch nicht abgespielt Nächstes Suchergebnis Vorheriges Suchergebnis @@ -3472,6 +3583,36 @@ Loslassen für Archiv Kleinere Größe Höhere Qualität + %1$s von %2$s + %1$s von %2$s heruntergeladen + %1$s von %2$s hochgeladen + %1$dx angezeigt + %1$dx angezeigt + %1$dx angezeigt + %1$dx angezeigt + %1$dx angezeigt + %1$d Bild senden + %1$d Bilder senden + %1$d Bilder senden + %1$d Bilder senden + %1$d Bilder senden + %1$d Datei senden + %1$d Dateien senden + %1$d Dateien senden + %1$d Dateien senden + %1$d Dateien senden + %1$d Audio senden + %1$d Audios senden + %1$d Audios senden + %1$d Audios senden + %1$d Audios senden + In %1$d Chat teilen + In %1$d Chats teilen + In %1$d Chats teilen + In %1$d Chats teilen + In %1$d Chats teilen + Richtige Antwort + Bild-in-Bild-Modus dd. MMM yyyy, h:mm a dd. MMM yyyy, HH:mm diff --git a/TMessagesProj/src/main/res/values-es/strings.xml b/TMessagesProj/src/main/res/values-es/strings.xml index ceb9ef61e..2b6fad3b6 100644 --- a/TMessagesProj/src/main/res/values-es/strings.xml +++ b/TMessagesProj/src/main/res/values-es/strings.xml @@ -37,7 +37,7 @@ ¿No recibiste el código? Enviar el código como un SMS No restablecer la cuenta - Alguien con acceso a tu número de teléfono **%1$s** solicitó eliminar tu cuenta de Telegram y restablecer tu contraseña de la verificación en dos pasos.\n\nSi no fuiste tú, por favor, inserta el código que enviamos por SMS a tu número. + Alguien con acceso a **%1$s** solicitó eliminar tu cuenta de Telegram y restablecer tu contraseña de la verificación en dos pasos.\n\nSi no fuiste tú, por favor, escribe el código que enviamos por SMS. También puedes cancelar la solicitud *cambiando tu número de teléfono*. Restablecer la cuenta Como la cuenta **%1$s** está activa y protegida con una contraseña, la eliminaremos en 1 semana, por motivos de seguridad.\n\nPuedes cancelar el proceso en cualquier momento. Podrás restablecer tu cuenta en: @@ -143,7 +143,7 @@ %s se unió a tu chat secreto. Te uniste al chat secreto. Vaciar chat - Vaciar chat + Borrar historial Historial eliminado. Silenciar Notificar @@ -223,6 +223,9 @@ Chats anclados Puedes anclar un número ilimitado de\nchats archivados en la parte superior. Mantén pulsado este botón para programar tu mensaje o enviarlo sin sonido. + ¿Ocultar nuevos chats? + Estás recibiendo muchos chats nuevos de usuarios que no están en tu lista de contactos. ¿Quieres **silenciar** y **archivar automáticamente** este tipo de chats? + IR A AJUSTES Promover a administrador Permisos de administrador @@ -326,6 +329,7 @@ Bots Otros miembros Otros suscriptores + se unió el %1$s Usuarios restringidos Administradores Eliminar canal @@ -339,6 +343,7 @@ Por favor, elige un enlace para tu canal público para que las personas puedan encontrarlo en la búsqueda y compartirlo con otros.\n\nSi no te interesa, te sugerimos crear un canal privado. Canal creado Foto del canal cambiada + Video del canal cambiado Foto del canal eliminada Nombre del canal cambiado a un2 Lo sentimos, tienes demasiados nombres de usuario públicos. Puedes anular el enlace de uno de tus grupos o canales anteriores, o crear uno privado. @@ -376,7 +381,8 @@ Eliminar del canal Lo sentimos, no puedes enviar mensajes en este canal. %1$s te añadió al canal %2$s - El canal %1$s actualizó su foto + El canal %1$s cambió su foto + El canal %1$s cambió su video %1$s publicó un mensaje %1$s publicó una foto %1$s publicó un video @@ -431,7 +437,7 @@ Invitar con un enlace Anclar mensajes Promovido por %1$s - No puedes editar los privilegios de este administrador. + No puedes editar los permisos de este administrador. No puedes editar este permiso. Este permiso no está disponible en grupos públicos. Eliminado por %1$s @@ -467,6 +473,8 @@ Gestionar grupo Gestionar canal Historial para nuevos miembros + Establecer nueva foto + Establecer foto o video Visible Los nuevos miembros verán mensajes que fueron enviados antes de unirse. Oculto @@ -487,7 +495,10 @@ Enlace Toca para añadir un enlace permanente Elegir foto + Elegir foto o video + Establecer nueva foto Tomar foto + Grabar video Subir desde Galería Elegir desde Galería Buscar en la web @@ -619,6 +630,8 @@ un1 se unió al canal un1 puso una nueva foto del grupo un1 puso una nueva foto del canal + un1 puso un nuevo video del grupo + un1 puso un nuevo video del canal un1 eliminó la foto del grupo un1 eliminó la foto del canal un1 editó este mensaje: @@ -702,10 +715,13 @@ Tu carpeta de música está vacía. No se encontraron resultados No hay coincidencias con **%1$s** en tu carpeta de música. + No hay coincidencias para **%1$s**. Música Artista desconocido Título desconocido - Aleatorio + Repetir canción + Repetir lista + Lista aleatoria Invertir orden Elige un archivo @@ -989,6 +1005,15 @@ Ocultar chat ¿Quieres ocultar este chat? Ocultar + Envía un mensaje o toca sobre el saludo de abajo para comenzar a chatear. + %1$s está %2$s + %1$s está %2$s + El chat ha sido movido a tu lista principal. + d + s + m + h + S %1$s activó la autodestrucción en %2$s Activaste la autodestrucción en %1$s @@ -1043,6 +1068,7 @@ %1$s te añadió al grupo %2$s %1$s renombró el grupo %2$s %1$s cambió la foto del grupo %2$s + %1$s cambió el video del grupo %2$s %1$s añadió a %3$s al grupo %2$s %1$s volvió al grupo %2$s %1$s se unió al grupo %2$s @@ -1051,7 +1077,7 @@ %1$s salió del 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 actualizó su foto de perfil + %1$s cambió su foto de perfil %1$s se unió al grupo %2$s con un enlace de invitación %1$s envió %3$s al grupo %2$s %1$s envió un álbum al grupo %2$s @@ -1147,6 +1173,11 @@ hasta %1$s Lo sentimos, este grupo está lleno. Lo sentimos, este chat no existe. + Este canal es privado. Únete a él para seguir viendo su contenido. + Este grupo es privado. Únete a él para seguir viendo su contenido. + Unirme al canal + Unirme al grupo + UNIRME Enlace copiado al portapapeles Enlace copiado al portapapeles.\nEste enlace sólo funcionará para miembros de este chat. Lamentablemente, no puedes acceder a este mensaje. No eres miembro del chat en el que fue publicado. @@ -1155,7 +1186,7 @@ Invitar al grupo con un enlace Enlace de invitación ¿Quieres anular este enlace? Una vez anulado, nadie podrá unirse a través de él. - El enlace de invitación anterior está inactivo. Ha sido creado uno nuevo. + El enlace de invitación anterior fue anulado. Se generó un enlace nuevo. Anular Anular enlace ¿Quieres anular el enlace **%1$s**?\n\nEl grupo “**%2$s**” pasará a ser privado. @@ -1834,11 +1865,12 @@ Escanea el código QR para conectar tu cuenta. Este código se puede utilizar para permitir que alguien inicie sesión en tu cuenta de Telegram.\n\nPara confirmar el inicio de sesión, ve a Ajustes > Dispositivos > Escanear QR y escanea el código. Telegram necesita acceso a tu cámara para que puedas escanear códigos QR. + Poner foto de perfil Base de datos local Borrar base de datos local ¿Quieres borrar los mensajes de texto en caché? - Vaciar la base de datos borrará los mensajes en caché y comprimirá la base de datos para liberar espacio de almacenamiento. Telegram requiere algunos datos para funcionar, así que la base de datos nunca podrá llegar a cero.\n\nEsta operación puede tomar unos minutos. + Borrar la base de datos eliminará los mensajes en caché y comprimirá la base de datos para ahorrar espacio de almacenamiento. Telegram requiere algunos datos para funcionar, así que la base de datos nunca podrá llegar a cero.\n\nEsta operación puede tomar unos minutos. Borrar caché Borrar Calculando... @@ -1851,7 +1883,7 @@ Otros archivos Vacía Conservar multimedia - Las fotos, videos y otros archivos de los chats en la nube, a los que **no accedas** durante este periodo, se eliminarán del dispositivo para ahorrar espacio.\n\nToda la multimedia estará en la nube de Telegram y podrás descargarla de nuevo si la necesitas. + Las fotos, videos y otros archivos de los chats en la nube a los que **no accedas** durante este periodo, se eliminarán del dispositivo para ahorrar espacio.\n\nToda la multimedia estará en la nube de Telegram y podrás descargarla de nuevo si la necesitas. Sin límite Mensajes de voz Videomensajes @@ -2222,10 +2254,14 @@ Mapa Satélite Híbrido - A %1$s m - A %1$s km - A %1$s ft - A %1$s mi + a %1$s m + a %1$s km + a %1$s ft + a %1$s mi + a %1$s m de ti + a %1$s km de ti + a %1$s ft de ti + a %1$s mi de ti Cómo llegar No se encontraron lugares No hay coincidencias con **%1$s** cerca de ti. @@ -2242,7 +2278,7 @@ Ubicación Lugar Exacta a %1$s - A %1$s + a %1$s O elige un lugar Toca para enviar esta ubicación Ubicaciones en tiempo real @@ -2328,6 +2364,7 @@ Grano Nitidez Desvanecer + Piel suave SOMBRAS ILUMINACIÓN TODO @@ -2343,7 +2380,11 @@ ¿Quieres eliminar este video? Eliminar GIF ¿Quieres eliminar este GIF? + ¿Quieres eliminar esta foto para todos? + ¿Quieres eliminar este video para todos? + ¿Quieres eliminar este GIF para todos? ¿Descartar cambios? + ¿Quieres descartar todos los cambios? ¿Quieres borrar el historial de búsqueda? ¿Quieres borrar tu historial de búsqueda? Historial de búsqueda @@ -2359,6 +2400,10 @@ Comentario Eliminar Editar + Bolígrafo + Resaltador + Neón + Flecha Duplicar Contorno Normal @@ -2370,6 +2415,15 @@ Agrupar multimedia en un mensaje Enviar sin agrupar Enviar sin compresión + Elige una portada para tu video de perfil + Establecer como principal + Abrir en el editor + Ahora esta es la foto principal de tu perfil. + Ahora este es el video principal de tu perfil. + Ahora esta es la foto principal del canal. + Ahora este es el video principal del canal. + Ahora esta es la foto principal del grupo. + Ahora este es el video principal del grupo. Verificación en dos pasos Verificación en dos pasos @@ -2404,7 +2458,7 @@ Tu contraseña para la verificación en dos pasos está activada. Tu contraseña de la verificación en dos pasos ha sido cambiada. Tu correo de recuperación para la verificación en dos pasos ahora está activo. - Tu correo de recuperación para la verificación en dos pasos fue cambiado. + Tu correo de recuperación para la verificación en dos pasos ha sido cambiado. Cambiar contraseña Desactivar contraseña Poner correo de recuperación @@ -2435,6 +2489,7 @@ Enviamos un código de recuperación al correo que nos diste:\n\n%1$s Por favor, revisa tu correo y pon el código de 6 dígitos que te enviamos. ¿Tienes problemas para acceder a tu correo %1$s? + ¿Tienes problemas para acceder a tu correo? Si no puedes acceder a tu correo, las opciones restantes son recordar tu contraseña o restablecer tu cuenta. RESTABLECER MI CUENTA Si continúas con el reinicio de tu cuenta, perderás todos tus chats y mensajes, junto con toda la multimedia y archivos que compartiste. @@ -2527,8 +2582,12 @@ Nadie (+%1$d) Seguridad Autodestrucción de la cuenta - Avanzados + Nuevos chats de desconocidos + Archivar y silenciar + Archiva y silencia automáticamente los nuevos chats, grupos y canales de quienes no son tus contactos. + Eliminar mi cuenta Eliminar mi cuenta si estoy fuera + Si estoy fuera Si no estás en línea al menos una vez durante este período, tu cuenta se eliminará junto con todos tus mensajes y contactos. ¿Quién puede ver mi últ. vez y estado en línea? Añadir excepciones @@ -2627,6 +2686,7 @@ un1 añadió a un2 un1 eliminó la foto del grupo un1 cambió la foto del grupo + un1 cambió el video del grupo un1 cambió el nombre del grupo a un2 un1 creó el grupo Eliminaste a un2 @@ -2638,6 +2698,7 @@ un1 puntuó %1$s en un2 Eliminaste la foto del grupo Cambiaste la foto del grupo + Cambiaste el video del grupo Cambiaste el nombre del grupo a un2 Creaste el grupo un1 te eliminó @@ -2818,7 +2879,7 @@ Telegram necesita acceso a la cámara para que puedas tomar fotos y videos. Por favor, actívalo en Ajustes. Telegram necesita acceso a tu ubicación para que puedas compartirla con tus amigos. Telegram necesita acceso a tu ubicación. - Telegram necesita acceso a mostrarse sobre otras aplicaciones para usar el modo Picture-in-Picture. + Telegram necesita acceso a mostrarse sobre otras aplicaciones para usar el modo imagen en imagen. AJUSTES Por favor, permite a Telegram aparecer en la pantalla bloqueada para que las llamadas funcionen correctamente. @@ -2848,6 +2909,52 @@ Base de datos local de Telegram • %s ¡Se liberaron %s en tu dispositivo! Otros datos • %s + Miembros del grupo + Nuevos miembros por fuente + Idioma principal de los miembros + Mensajes + Acciones + Miembros + Mensajes + Miembros que leen + Miembros que publican + %1$d caracteres + %1$d carácter + %1$d caracteres + %1$d caracteres + %1$d caracteres + %1$d caracteres + %1$d eliminaciones + %1$d eliminación + %1$d eliminaciones + %1$d eliminaciones + %1$d eliminaciones + %1$d eliminaciones + %1$d suspensiones + %1$d suspensión + %1$d suspensiones + %1$d suspensiones + %1$d suspensiones + %1$d suspensiones + %1$d restricciones + %1$d restricción + %1$d restricciones + %1$d restricciones + %1$d restricciones + %1$d restricciones + %1$d añadidos + %1$d añadido + %1$d añadidos + %1$d añadidos + %1$d añadidos + %1$d añadidos + Top admins. + Top miembros + Top miembros que añaden + %s por mensaje + Top días de la semana + Ver mensajes + Abrir perfil Telegram Rápida @@ -3392,6 +3499,7 @@ Chat secreto Enviado el %s Recibido el %s + Programado para el %s Volver atrás Abrir menú de navegación %2$s por %1$s @@ -3448,6 +3556,7 @@ Contraer panel Opciones de usuario Rotar + Imagen invertida Editor de fotos Ajustes Visor de fotos @@ -3462,6 +3571,8 @@ Tomar una foto más Visto No visto + Enviando + Enviando error No reproducido Siguiente resultado de búsqueda Anterior resultado de búsqueda @@ -3472,6 +3583,36 @@ Suelta para ver el archivo Menor tamaño Mejor calidad + %1$s de %2$s + %1$s de %2$s descargado + %1$s de %2$s subido + Visto %1$d vez + Visto %1$d veces + Visto %1$d veces + Visto %1$d veces + Visto %1$d veces + Enviar %1$d foto + Enviar %1$d fotos + Enviar %1$d fotos + Enviar %1$d fotos + Enviar %1$d fotos + Enviar %1$d archivo + Enviar %1$d archivos + Enviar %1$d archivos + Enviar %1$d archivos + Enviar %1$d archivos + Enviar %1$d audio + Enviar %1$d audios + Enviar %1$d audios + Enviar %1$d audios + Enviar %1$d audios + Compartir en %1$d chat + Compartir en %1$d chats + Compartir en %1$d chats + Compartir en %1$d chats + Compartir en %1$d chats + Respuesta correcta + Modo imagen en imagen dd MMM yyyy, h:mm a dd MMM yyyy, HH:mm diff --git a/TMessagesProj/src/main/res/values-it/strings.xml b/TMessagesProj/src/main/res/values-it/strings.xml index e4f1a2a25..761f78a1e 100644 --- a/TMessagesProj/src/main/res/values-it/strings.xml +++ b/TMessagesProj/src/main/res/values-it/strings.xml @@ -37,7 +37,7 @@ Non hai ricevuto il codice? Invia il codice come SMS Annulla ripristino account - Qualcuno con accesso al tuo numero di telefono **%1$s** ha richiesto l\'eliminazione del tuo account Telegram e il ripristino della password della verifica in due passaggi.\n\nSe non sei stato tu, per favore inserisci il codice che abbiamo appena inviato tramite SMS al tuo numero. + Qualcuno con accesso a **%1$s** ha richiesto l\'eliminazione del tuo account Telegram e il ripristino della password della verifica in due passaggi.\n\nSe non sei stato tu, per favore inserisci il codice che abbiamo appena inviato tramite SMS. Puoi anche annullare *cambiando il tuo numero di telefono*. Ripristino account Dato che l\'account **%1$s** è attivo e protetto da una password, lo elimineremo tra 1 settimana per motivi di sicurezza.\n\nPuoi annullare questo processo in qualsiasi momento. Potrai ripristinare il tuo account tra: @@ -48,7 +48,7 @@ Il tuo codice di accesso è **%1$s**. Inseriscilo nell\'app Telegram in cui stai cercando di accedere.\n\nNon dare questo codice a nessuno. Il tuo nome - Inserisci il tuo nome e aggiungi una foto profilo. + Inserisci il tuo nome e aggiungi una foto del profilo. Nome (obbligatorio) Cognome (facoltativo) Annulla iscrizione @@ -223,6 +223,9 @@ Chat fissate Puoi fissare in alto un numero illimitato di chat archiviate. Premi questo pulsante per programmare il tuo messaggio o per inviarlo senza suono. + Nascondere le nuove chat? + Stai ricevendo molte nuove chat da utenti che non sono tra i tuoi contatti. Vuoi che queste chat siano **automaticamente silenziate** e **archiviate**? + VAI ALLE IMPOSTAZIONI Rendi amministratore Modifica permessi amministratore @@ -326,6 +329,7 @@ Bot Altri membri Altri iscritti + unito il %1$s Utenti limitati Amministratori Elimina canale @@ -339,6 +343,7 @@ 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 + Video del canale cambiato Foto del canale rimossa Nome del canale cambiato in un2 Spiacenti, hai riservato troppi username pubblici. Puoi revocare il link da uno dei tuoi gruppi o canali più vecchi, o creare invece delle entità private. @@ -377,6 +382,7 @@ Spiacenti, non puoi inviare messaggi in questo canale %1$s ti ha aggiunto al canale %2$s Il canale %1$s ha aggiornato la sua foto + Il canale %1$s ha aggiornato il suo video %1$s ha pubblicato un messaggio %1$s ha pubblicato una foto %1$s ha pubblicato un video @@ -467,6 +473,8 @@ Gestisci gruppo Gestisci canale Cronologia per i nuovi membri + Imposta nuova foto + Imposta foto o video Visibile I nuovi membri vedranno i messaggi che sono stati inviati prima che si unissero. Nascosta @@ -487,7 +495,10 @@ Link Tocca per aggiungere un link permanente Scegli foto + Scegli una foto o un video + Imposta nuova foto Scatta foto + Registra video Carica dalla galleria Seleziona dalla galleria Ricerca web @@ -619,6 +630,8 @@ un1 si è unito al canale un1 ha impostato una nuova foto del gruppo un1 ha impostato una nuova foto del canale + un1 ha impostato un nuovo video del gruppo + un1 ha impostato un nuovo video del canale un1 ha rimosso la foto del gruppo un1 ha rimosso la foto del canale un1 ha modificato questo messaggio: @@ -702,10 +715,13 @@ La tua libreria musicale è vuota. Nessun risultato trovato Non ci sono corrispondenze per **%1$s** nella tua libreria musicale. + Non ci sono risultati per **%1$s**. Musica Artista sconosciuto Titolo sconosciuto - Casuale + Ripeti canzone + Ripeti lista + Lista casuale Inverti ordine Seleziona file @@ -743,7 +759,7 @@ Riprova o scegli dalla lista seguente invisibile - assistenza + supporto sta scrivendo... sta scrivendo... stanno scrivendo... @@ -909,7 +925,7 @@ Elimina per %1$s Elimina per tutti i membri Testo copiato negli appunti - Hold to record audio. + Tieni premuto per l\'audio. Tieni premuto per l\'audio. Tocca per il video. Tieni premuto per il video. Tocca per l\'audio. Scarta messaggio vocale @@ -989,6 +1005,15 @@ Nascondi chat Sei sicuro di voler nascondere questa chat? Nascondi + Invia un messaggio o tocca il saluto qui sotto per mostrare che sei pronto a chattare. + %1$s è a %2$s + %1$s è %2$s + La chat è stata spostata nella tua lista principale. + g + s + m + h + S %1$s ha impostato il timer di autodistruzione a %2$s Hai impostato il timer di autodistruzione a %1$s @@ -1043,6 +1068,7 @@ %1$s ti ha invitato nel gruppo %2$s %1$s ha rinominato il gruppo %2$s %1$s ha modificato la foto del gruppo %2$s + %1$s ha cambiato il video del gruppo per %2$s %1$s ha invitato %3$s nel gruppo %2$s %1$s è tornato nel gruppo %2$s %1$s si è unito al gruppo %2$s @@ -1147,6 +1173,11 @@ fino a %1$s Spiacenti, questo gruppo è già pieno. Spiacenti, sembra che questa chat non esista. + Questo canale è privato. Unisciti per continuare a vederne i contenuti. + Questo gruppo è privato. Unisciti per continuare a vederne i contenuti. + Unisciti al canale + Unisciti al gruppo + UNISCITI Link copiato negli appunti Link copiato negli appunti.\nQuesto link funzionerà solo per i membri di questa chat. Sfortunatamente, non puoi accedere a questo messaggio. Non sei membro della chat in cui è stato pubblicato. @@ -1518,7 +1549,7 @@ Esci Nessun suono Default - Assistenza + Supporto Solo se silenzioso Sfocato Prospettiva @@ -1582,7 +1613,7 @@ Questa lingua non esiste. Stai già usando questa lingua (**%1$s**). Puoi cambiare la tua lingua in qualsiasi momento nelle Impostazioni. Sfortunatamente, questa lingua personalizza (**%1$s**) non contiene dati per Telegram Android. - Per favore nota che l\'assistenza di Telegram è gestita da volontari. Proviamo a rispondere quanto prima, ma potrebbe volerci del tempo.\n\nDai un\'occhiata alle FAQ di Telegram]]>: contengono suggerimenti importanti per risolvere i problemi]]> e risposte a quasi tutte le domande. + Per favore nota che il supporto di Telegram è gestito da volontari. Proviamo a rispondere quanto prima, ma potrebbe volerci del tempo.\n\nDai un\'occhiata alle FAQ di Telegram]]>: contengono suggerimenti importanti per risolvere i problemi]]> e risposte a quasi tutte le domande. Chiedi a un volontario FAQ Telegram FAQ Telegram @@ -1808,7 +1839,7 @@ Libera spazio sul tuo dispositivo; i tuoi media rimarranno nel cloud. Cambia numero di telefono Sposta il tuo account, le chat e i media su un nuovo numero. - Contatta l\'assistenza + Contatta il supporto Segnalaci qualsiasi problema; disconnettersi solitamente non aiuta. Esci Uscendo disattiverai tutte le chat segrete. @@ -1834,6 +1865,7 @@ Scansiona il codice QR per connettere il tuo account. Questo codice può essere usato per consentire a qualcuno di accedere al tuo account Telegram.\n\nPer confermare l\'accesso a Telegram, vai in Impostazioni > Dispositivi > Scansiona QR e scansiona il codice. Telegram deve accedere alla tua fotocamera affinché tu possa scansionare i codici QR . + Imposta foto del profilo Database locale Cancella database locale @@ -2222,10 +2254,14 @@ Mappa Satellite Ibrido - Distante %1$s m - Distante %1$s km - Distante %1$s piedi - Distante %1$s miglia + distante %1$s m + distante %1$s km + distante %1$s piedi + distante %1$s miglia + %1$s m da te + %1$s km da te + %1$s ft da te + %1$s mi da te Indicazioni Nessun luogo trovato Non ci sono risultati per **%1$s** vicino a te. @@ -2328,6 +2364,7 @@ Grana Nitidezza Sfumatura + Pelle morbida OMBRE ALTE LUCI TUTTO @@ -2343,7 +2380,11 @@ Sei sicuro di voler eliminare questo video? Elimina GIF Sei sicuro di voler eliminare questa GIF? + Sei sicuro di voler eliminare questa foto per tutti? + Sei sicuro di voler eliminare questo video per tutti? + Sei sicuro di voler eliminare questa GIF per tutti? Scartare le modifiche? + Sei sicuro di voler scartare tutte le modifiche? Cancellare la cronologia di ricerca? Sei sicuro di voler cancellare la tua cronologia di ricerca? Cancella cronologia chat @@ -2359,6 +2400,10 @@ Didascalia Elimina Modifica + Penna + Evidenziatore + Neon + Freccia Duplica Contornato Normale @@ -2370,6 +2415,15 @@ Mostra i media come messaggio unico Invia senza raggruppare Invia senza compressione + Scegli una copertina per il tuo video del profilo + Imposta come principale + Apri nell\'editor + Questa è ora la tua foto principale. + Questo è ora il tuo video principale. + Questa è ora la foto principale del canale. + Questo è ora il video principale del canale. + Questa è ora la foto principale del gruppo. + Questo è ora il video principale del gruppo. Verifica in due passaggi Verifica in due passaggi @@ -2379,7 +2433,7 @@ Ritorna al Passport Imposta password Imposta password aggiuntiva - Puoi impostare una password che ti verrà richiesta quando ti connetti da un nuovo dispositivo in aggiunta al codice che riceverai via SMS. + Puoi impostare una password che sarà richiesta quando accedi su un nuovo dispositivo oltre al codice che ricevi via SMS. La tua password No @@ -2404,7 +2458,7 @@ La password per la verifica in due passaggi è ora attiva. La tua password per la verifica in due passaggi è stata cambiata. La tua email di recupero per la verifica in due passaggi è ora attiva. - Your recovery email for Two-Step Verification is changed. + La tua email di recupero per la verifica in due passaggi è stata cambiata. Cambia password Disattiva password Imposta email di recupero @@ -2435,6 +2489,7 @@ Abbiamo inviato un codice di ripristino all\'email che ci hai fornito:\n\n%1$s Per favore controlla la tua email e inserisci il codice a 6 cifre che ti abbiamo inviato lì. Hai problemi ad accedere alla tua email %1$s? + Hai problemi ad accedere alla tua email? Se non puoi ripristinare l\'accesso alla tua email, non ti resta che ricordare la tua password o ripristinare il tuo account. RIPRISTINA IL MIO ACCOUNT Perderai tutte le chat e i messaggi, insieme ai media e ai file condivisi, se procederai a ripristinare il tuo account. @@ -2479,9 +2534,9 @@ Privacy e sicurezza Privacy Ultimo accesso e in linea - Foto profilo - Chi può vedere la mia foto profilo? - Puoi decidere chi può vedere la tua foto profilo con precisione granulare. + Foto del profilo + Chi può vedere la mia foto del profilo? + Puoi decidere chi può vedere la tua foto del profilo con precisione granulare. Puoi aggiungere utenti o interi gruppi come eccezioni che annulleranno le impostazioni precedenti. Numero di telefono Chi può vedere il mio numero? @@ -2527,8 +2582,12 @@ Nessuno (+%1$d) Sicurezza Elimina il mio account - Avanzate + Nuove chat da utenti sconosciuti + Archivia e silenzia + Archivia e silenzia automaticamente nuove chat, gruppi e canali da non contatti. + Elimina il mio account Elimina il mio account se lontano per + Se lontano per Se non ti connetti almeno una volta in questo periodo, il tuo account verrà eliminato insieme a tutti i messaggi e i contatti. Chi può vedere il tuo ultimo accesso? Aggiungi eccezioni @@ -2627,6 +2686,7 @@ un1 ha aggiunto un2 un1 ha rimosso la foto del gruppo un1 ha cambiato la foto del gruppo + un1 ha cambiato il video del gruppo un1 ha cambiato il nome del gruppo in un2 un1 ha creato il gruppo Hai rimosso un2 @@ -2638,6 +2698,7 @@ un1 ha totalizzato %1$s a un2 Hai rimosso la foto del gruppo Hai cambiato la foto del gruppo + Hai cambiato il video del gruppo Hai cambiato il nome del gruppo in un2 Hai creato il gruppo un1 ti ha rimosso @@ -2848,6 +2909,52 @@ %s Database locale di Telegram %s liberati sul tuo dispositivo! %s Altri dati + Membri del gruppo + Nuovi membri per origine + Lingua principale dei membri + Messaggi + Azioni + Membri + Messaggi + Membri che visualizzano + Membri che pubblicano + %1$d simboli + %1$d simbolo + %1$d simboli + %1$d simboli + %1$d simboli + %1$d simboli + %1$d eliminazioni + %1$d eliminazione + %1$d eliminazioni + %1$d eliminazioni + %1$d eliminazioni + %1$d eliminazioni + %1$d blocchi + %1$d blocco + %1$d blocchi + %1$d blocchi + %1$d blocchi + %1$d blocchi + %1$d limitazioni + %1$d limitazione + %1$d limitazioni + %1$d limitazioni + %1$d limitazioni + %1$d limitazioni + %1$d inviti + %1$d invito + %1$d inviti + %1$d inviti + %1$d inviti + %1$d inviti + Amministratori migliori + Membri migliori + Top inviters + %s per messaggio + Giorni migliori della settimana + Visualizza messaggi + Apri profilo Telegram Veloce @@ -3392,6 +3499,7 @@ Chat segreta Inviato %s Ricevuto %s + Programmato per %s Torna indietro Apri menù di navigazione %2$s di %1$s @@ -3413,8 +3521,8 @@ Registra un messaggio vocale Registra videomessaggio Apri chat - Foto profilo - Cambia foto profilo + Foto del profilo + Cambia foto del profilo Cambia ordine Mostra account Nascondi account @@ -3448,6 +3556,7 @@ Chiudi pannello Opzioni utente Ruota + Specchio Editor foto Aggiustamenti Visualizzatore foto @@ -3462,6 +3571,8 @@ Scatta un\'altra foto Visto Non visto + Invio + Errore di invio Non riprodotto Risultato della ricerca successivo Risultato della ricerca precedente @@ -3472,6 +3583,36 @@ Rilascia per l’archivio Dimensione minore Qualità maggiore + %1$s di %2$s + Scaricato %1$s di %2$s + Caricato %1$s di %2$s + Visualizzato %1$d volta + Visualizzato %1$d volte + Visualizzato %1$d volte + Visualizzato %1$d volte + Visualizzato %1$d volte + Invia %1$d foto + Invia %1$d foto + Invia %1$d foto + Invia %1$d foto + Invia %1$d foto + Invia %1$d file + Invia %1$d file + Invia %1$d file + Invia %1$d file + Invia %1$d file + Invia %1$d file audio + Invia %1$d file audio + Invia %1$d file audio + Invia %1$d file audio + Invia %1$d file audio + Condividi in %1$d chat + Condividi in %1$d chat + Condividi in %1$d chat + Condividi in %1$d chat + Condividi in %1$d chat + Risposta corretta + Modalità Picture-in-Picture dd MMM yyyy, h:mm a dd MMM yyyy, HH:mm diff --git a/TMessagesProj/src/main/res/values-ko/strings.xml b/TMessagesProj/src/main/res/values-ko/strings.xml index b07d50fd8..0e7ef1901 100644 --- a/TMessagesProj/src/main/res/values-ko/strings.xml +++ b/TMessagesProj/src/main/res/values-ko/strings.xml @@ -37,7 +37,7 @@ 코드를 받지 못하셨나요? 코드를 SMS로 보내기 계정 초기화 취소 - 전화번호 **%1$s**에 접근할 권한이 있는 누군가가 회원님의 텔레그램 계정을 삭제하고 2단계 인증 비밀번호를 초기화하도록 요청했습니다.\n\n본인이 아닌 경우, 방금 SMS를 통해 회원님의 번호로 보내 드린 코드를 입력해 주세요. + Somebody with access to **%1$s** has requested to delete your Telegram account and reset your 2-Step Verification password.\n\nIf this wasn\'t you, please enter the code we\'ve just sent you via SMS. You can also cancel this by *changing your phone number*. 계정 초기화 계정 **%1$s**이 비밀번호로 보호되어 있기에, 보안 목적으로 1주일 뒤에 이 계정을 삭제할 것입니다.\n\n이 작업은 언제든지 취소하실 수 있습니다. 아래 시간이 지나야 계정을 초기화하실 수 있습니다: @@ -223,6 +223,9 @@ 고정된 대화방 보관한 대화방들은 개수 제한 없이 맨 위에 고정하실 수 있습니다. 이 버튼을 길게 눌러 메시지를 예약하거나 메시지를 소리 없이 보내세요. + Hide new chats? + 연락처 목록에 없는 사용자에게서 새로운 메시지를 많이 받고 계십니다. 해당 대화방에 **자동 알림 끄기**와 **보관**을 적용하시겠습니까? + GO TO SETTINGS 관리자로 승격 관리자 권한 수정 @@ -326,6 +329,7 @@ 나머지 참가자 나머지 구독자 + joined %1$s 제한된 사용자 관리자 채널 삭제 @@ -339,6 +343,7 @@ 사람들이 이 채널을 검색하여 찾고 공유할 수 있도록 링크를 설정해 주세요.\n\n이를 꺼리신다면, 비공개 채널을 추천해 드립니다. 채널이 만들어졌습니다 채널 사진이 바뀌었습니다 + Channel video changed 채널 사진이 제거되었습니다 채널명이 un2(으)로 바뀌었습니다 죄송하지만, 공개 사용자명을 너무 많이 가지셨습니다. 다른 그룹이나 채널의 링크를 하나 폐기하거나, 비공개 그룹으로 바꾸세요. @@ -377,6 +382,7 @@ 죄송합니다. 이 채널에 메시지를 보내실 수 없습니다. %1$s 님이 나를 %2$s 채널에 초대했습니다 %1$s 채널 사진이 업데이트되었습니다 + Channel %1$s updated its video %1$s에서 메시지를 게시했습니다 %1$s에서 사진을 게시했습니다 %1$s 채널이 동영상을 게시했습니다 @@ -467,6 +473,8 @@ 그룹 관리 채널 관리 새로운 참가자에게 기존 대화 내용을 + Set New Photo + Set Photo or Video 보임 새로운 참가자는 들어오기 전부터 있던 메시지를 볼 수 있습니다. 숨김 @@ -487,7 +495,10 @@ 링크 영구 링크를 추가하려면 누르세요 사진 선택 + Choose photo or video + Set new photo 카메라 + Record video 갤러리 갤러리 웹 검색 @@ -619,6 +630,8 @@ un1 님이 채널에 들어왔습니다 un1 님이 그룹 사진을 새로 설정했습니다 un1 님이 채널 사진을 새로 설정했습니다 + un1 set a new group video + un1 set a new channel video un1 님이 그룹 사진을 제거했습니다 un1 님이 채널 사진을 제거했습니다 un1 님이 아래 메시지를 수정했습니다: @@ -702,10 +715,13 @@ 음악 라이브러리가 비었습니다 검색 결과가 없습니다 음악 라이브러리에 **%1$s**와 같은 것이 없습니다. + There are no matches for **%1$s**. 음악 알 수 없는 아티스트 알 수 없는 제목 - 섞기 + Repeat song + Repeat list + Shuffle list 역순 파일 선택 @@ -971,7 +987,7 @@ 📅 알림 1건의 결과 압축 설정을 바꾸기엔 동영상 품질이 너무 낮습니다. - **낱말**을 꾹 잡고선, 커서를 움직여 복사할 글을 늘리세요. + **낱말**을 꾹 잡고선, 커서를 움직여 복사할 글을 조정하세요. 카드 번호가 클립보드에 복사되었습니다 복사 대화방에 **:dice:**를 보내어 주사위를 굴리세요. @@ -989,6 +1005,15 @@ 대화방 가리기 정말 이 대화방을 가리시겠습니까? 가리기 + Send a message or tap on the greeting below to show that you are ready to chat. + %1$s is %2$s + %1$s is %2$s + The chat was moved to your main list. + d + s + m + h + w %1$s 님이 자동 삭제 타이머를 %2$s(으)로 맞췄습니다 자동 삭제 타이머를 %1$s(으)로 맞추셨습니다 @@ -1043,6 +1068,7 @@ %1$s님이 %2$s 그룹에 초대했습니다 %1$s 님이 %2$s 그룹명을 바꿨습니다 %1$s 님이 %2$s 그룹 사진을 바꿨습니다 + %1$s changed the group video for %2$s %1$s 님이 %3$s 님을 %2$s 그룹에 초대했습니다 %1$s 님이 %2$s 그룹에 돌아왔습니다 %1$s 님이 %2$s 그룹에 들어왔습니다 @@ -1147,6 +1173,11 @@ 최대 %1$s 죄송합니다. 이 그룹은 이미 가득 찼습니다. 죄송하지만, 대화방이 없어진 모양입니다. + This channel is private. Please join it to continue viewing its content. + This group is private. Please join it to continue viewing its content. + Join Channel + Join Group + JOIN 링크를 클립보드에 복사했습니다 링크가 클립보드에 복사되었습니다.\n이 링크는 이 대화방의 참가자에게만 작동합니다. 안타깝게도 이 메시지에 접근하실 수 없습니다. 회원님은 메시지가 게시된 대화방의 참가자가 아닙니다. @@ -1776,7 +1807,7 @@ 개인 대화 그룹 대화 채널 - 용량 제한 + 크기 제한 최대 %1$s 스트리밍 동영상 및 음악 파일 스트리밍 @@ -1834,6 +1865,7 @@ 회원님의 계정을 연결하려면 QR 코드를 스캔하세요. 이 코드는 누군가가 회원님의 텔레그램 계정에 로그인할 수 있도록 하는 데 사용될 수 있습니다.\n\n텔레그램 로그인을 인증하려면, 설정 > 디바이스 > QR 코드 스캔으로 이동하여 코드를 스캔해 주세요. Telegram needs access to your camera so that you can scan QR codes. + Set Profile Photo 로컬 데이터베이스 로컬 데이터베이스 비우기 @@ -1851,7 +1883,7 @@ 다른 파일 비어 있음 미디어 저장 기간 - 이 기간 동안 회원님이 **접근하지 않으신** 클라우드 대화방의 사진, 동영상과 그 밖의 파일들이 디스크 공간을 절약하기 위해 이 기기에서 제거됩니다.\n\n모든 미디어는 텔레그램 클라우드에 남으며 언제든지 다시 다운로드하실 수 있습니다. + 이 기간 동안 회원님이 **접근하지 않으신** 클라우드 대화방의 사진, 동영상과 그 밖의 파일들이 디스크 공간 확보를 위해 이 기기에서 제거됩니다.\n\n모든 미디어는 텔레그램 클라우드에 남으며 필요할 때 다시 다운로드하실 수 있습니다. 무기한 음성 메시지 동영상 메시지 @@ -2146,13 +2178,13 @@ *설정 > 폴더*를 열어 대화방을 폴더로 정리하세요. 알림 켜짐 읽지 않음 - Add to folder - Remove from folder + 폴더에 추가 + 폴더에서 제거 Choose a folder **%1$s** added to **%2$s** **%1$s** added to **%2$s** **%1$s** added to **%2$s** - **%1$s** removed from **%2$s** + **%1$s**을(를) **%2$s**에서 제거했습니다 **%1$s** removed from **%2$s** **%1$s** removed from **%2$s** Limit reached @@ -2226,6 +2258,10 @@ %1$s km 떨어짐 %1$s ft 떨어짐 %1$s mi 떨어짐 + %1$s m from you + %1$s km from you + %1$s ft from you + %1$s mi from you 가는 길 장소를 찾지 못했습니다 주변에 **%1$s** 같은 곳이 없습니다. @@ -2252,7 +2288,7 @@ 8시간 동안 갱신됨 %1$s 전 갱신됨 - 방금 갱신됨 + 방금 업데이트됨 나와 %1$s 님 %1$s를 %2$s와 공유 중 모두 중단 @@ -2328,6 +2364,7 @@ 그레인 선명 흐림 + Soften Skin 그림자 하이라이트 모두 @@ -2343,7 +2380,11 @@ 이 동영상을 정말 삭제하시겠습니까? GIF 삭제 정말 이 GIF를 삭제하시겠습니까? + Are you sure you want to delete this photo for everyone? + Are you sure you want to delete this video for everyone? + Are you sure you want to delete this GIF for everyone? 변경 사항을 삭제할까요? + Are you sure you want to discard all changes? 검색 기록을 비울까요? 정말 검색 기록을 비우시겠습니까? 검색 기록 비우기 @@ -2359,10 +2400,14 @@ 설명 삭제 수정 + Pen + Marker + Neon + Arrow 복제 외곽선 일반 - Framed + 프레임 초기화 원본 정방형 @@ -2370,6 +2415,15 @@ 미디어를 하나의 메시지로 묶기 묶지 않고 보내기 압축 없이 보내기 + Choose a cover for your profile video + Set as main + Open in editor + This is your main profile photo now. + This is your main profile video now. + This is the main channel photo now. + This is the main channel video now. + This is the main group photo now. + This is the main group video now. 2단계 인증 2단계 인증 @@ -2404,7 +2458,7 @@ 2단계 인증 비밀번호가 활성화되었습니다. Your password for Two-Step Verification has been changed. 2단계 인증용 복구 이메일이 이제 작동합니다. - Your recovery email for Two-Step Verification is changed. + Your recovery email for Two-Step Verification has been changed. 비밀번호 바꾸기 비밀번호 끄기 복구 이메일 설정하기 @@ -2435,6 +2489,7 @@ 제공하신 이메일로 복구 코드를 보냈습니다:\n\n%1$s 이메일을 확인하여 보내 드린 6자리 코드를 입력해 주세요. %1$s 이메일에 접근하는 데 문제가 있으신가요? + Having trouble accessing your email? 이메일에 접근하지 못한다면, 비밀번호를 기억해 내거나 계정을 초기화하시는 수밖에 없습니다. 계정 초기화 계정 초기화를 진행하시면, 대화방, 메시지와 더불어 공유한 미디어와 파일이 모두 사라집니다. @@ -2527,8 +2582,12 @@ 없음 (+%1$d) 보안 계정 자동 탈퇴 - 고급 + New chats from unknown users + Archive and Mute + Automatically archive and mute new chats, groups and channels from non-contacts. + Delete my account 다음 기간 내 미접속 시 계정 탈퇴 + If away for 이 기간 안에 한 번이라도 접속하지 않으시면, 모든 그룹, 메시지, 연락처와 더불어 회원님의 계정이 사라집니다. 내 마지막 접속 시간을 볼 수 있는 사람 예외 추가 @@ -2627,6 +2686,7 @@ un1 님이 un2 님을 추가했습니다 un1 님이 그룹 사진을 제거했습니다 un1 님이 그룹 사진을 바꿨습니다 + un1 changed the group video un1 님이 그룹명을 un2(으)로 바꿨습니다 un1 님이 그룹을 만들었습니다 un2 님을 추방하셨습니다 @@ -2638,6 +2698,7 @@ un1님이 un2에서 %1$s점 획득 그룹 사진을 삭제하셨습니다 그룹 사진을 바꾸셨습니다 + You changed the group video 그룹명을 un2(으)로 바꾸셨습니다 그룹을 만드셨습니다 un1 님이 나를 추방했습니다 @@ -2680,7 +2741,7 @@ 회원님이 스크린샷을 찍었습니다! un1 님이 스크린샷을 찍었습니다! - 한계에 이름 + 상한선에 다다름 활동 없는 대화방 채널, %1$s 간 활동 없음 %1$s, %2$s 간 활동 없음 @@ -2692,7 +2753,7 @@ 죄송합니다. 해당 사용자가 너무 많은 그룹과 채널에 들어가 있습니다.\n먼저 몇 곳을 나가도록 요청해 주세요. 잠깐! **참가자 서로에게서** 이 대화방의 **모든 메시지가 삭제**됩니다. - Warning! This will **delete all messages** in this chat. + 경고! 이 대화방의 **모든 메시지**가 삭제됩니다. 모두 삭제 불러오기를 중단할까요? 텔레그램을 업데이트하세요 @@ -2848,6 +2909,52 @@ %s 텔레그램 로컬 데이터베이스 %s를 회원님 기기에 확보했습니다! %s 기타 데이터 + Group members + New members by source + Members\' primary language + Messages + Actions + Members + Messages + Viewing members + Posting Members + %1$d symbols + %1$d symbol + %1$d symbols + %1$d symbols + %1$d symbols + %1$d symbols + %1$d deletions + %1$d deletion + %1$d deletions + %1$d deletions + %1$d deletions + %1$d deletions + %1$d bans + %1$d ban + %1$d bans + %1$d bans + %1$d bans + %1$d bans + %1$d restrictions + %1$d restriction + %1$d restrictions + %1$d restrictions + %1$d restrictions + %1$d restrictions + %1$d invitations + %1$d invitation + %1$d invitations + %1$d invitations + %1$d invitations + %1$d invitations + Top admins + Top members + Top inviters + %s per message + Top days of week + View Messages + Open Profile 텔레그램 눈부신 속도 @@ -3392,6 +3499,7 @@ 비밀 대화 %s 보냄 %s 받음 + Scheduled for %s 돌아가기 네비게이션 메뉴 열기 %2$s로 %1$s @@ -3448,6 +3556,7 @@ 패널 모으기 사용자 옵션 회전 + Mirror 사진 편집기 조정 사진 뷰어 @@ -3462,6 +3571,8 @@ 사진 한 장 더 찍기 접속함 접속 안 함 + Sending + Sending error 미재생 다음 검색 결과 이전 검색 결과 @@ -3472,6 +3583,36 @@ 놓아서 보관함 보기 더 작은 크기 나은 품질 + %1$s of %2$s + Downloaded %1$s of %2$s + Uploaded %1$s of %2$s + Viewed %1$d time + Viewed %1$d times + Viewed %1$d times + Viewed %1$d times + Viewed %1$d times + Send %1$d photo + Send %1$d photos + Send %1$d photos + Send %1$d photos + Send %1$d photos + Send %1$d file + Send %1$d files + Send %1$d files + Send %1$d files + Send %1$d files + Send %1$d audio file + Send %1$d audio files + Send %1$d audio files + Send %1$d audio files + Send %1$d audio files + Share in %1$d chat + Share in %1$d chats + Share in %1$d chats + Share in %1$d chats + Share in %1$d chats + Correct answer + Picture-in-Picture mode yyyy년 MMM dd일 h:mm a yyyy년 MMM dd일 HH:mm diff --git a/TMessagesProj/src/main/res/values-nl/strings.xml b/TMessagesProj/src/main/res/values-nl/strings.xml index b1756fc88..076b56720 100644 --- a/TMessagesProj/src/main/res/values-nl/strings.xml +++ b/TMessagesProj/src/main/res/values-nl/strings.xml @@ -37,7 +37,7 @@ Geen code ontvangen? Verstuur de code per SMS Account reset annuleren - Iemand met toegang tot nummer **%1$s** heeft geprobeerd om je Telegram account te verwijderen en je twee-staps-verificatie wachtwoord te resetten.\n\nAls jij dit niet was, geef dan de code in die we zojuist per SMS hebben gestuurd. + Iemand met toegang tot nummer **%1$s** heeft geprobeerd om je Telegram account te verwijderen en je twee-staps-verificatie wachtwoord te resetten.\n\nAls jij dit niet was, geef dan de code in die we zojuist per SMS hebben gestuurd. Je kunt dit ook annuleren door je telefoonnummer te *wijzigen naar een nummer waar je toegang tot hebt*. Account resetten Account **%1$s** is actief en beveiligd met een wachtwoord, het verwijderen stellen we daarom 1 week uit.\n\nJe kunt dit proces ieder moment annuleren. Je account kan gereset worden over: @@ -223,6 +223,9 @@ Vastgezette chats Je kunt een ongelimteerd aantal gearchiveerde chats vastzetten. Hou deze knop ingedrukt om je bericht te plannen of zonder geluid te sturen. + Nieuwe chats verbergen? + Je ontvangt veel nieuwe chats van mensen die geen contact van je zijn. Wil je deze chat **automatisch dempen** en **archiveren**? + GA NAAR INSTELLINGEN Promoveren tot beheerder Beheerdersrechten aanpassen @@ -326,6 +329,7 @@ Bots Andere leden Andere abonnees + lid geworden op %1$s Beperkt Beheerders Kanaal verwijderen @@ -339,6 +343,7 @@ Stel een link in voor je publieke kanaal, om deze vindbaar te maken via de zoekfunctie en te delen met anderen.\n\nWil je dit niet dan kun je een privé-kanaal aanmaken. Kanaal gemaakt Kanaalfoto bijgewerkt + Kanaalvideo bijgewerkt Kanaalfoto verwijderd Kanaalnaam gewijzigd naar un2 Het maximale aantal publieke gebruikersnamen is bereikt. Trek eerst een link van één van je oudere groepen of kanalen in, of gebruik de privé-variant. @@ -377,6 +382,7 @@ Je hebt alleen leesrechten in dit kanaal. %1$s heeft je toegevoegd aan het kanaal %2$s Kanaalfoto van %1$s bijgewerkt + Kanaal %1$s heeft zijn video bijgewerkt %1$s plaatste een bericht %1$s plaatste een foto %1$s plaatste een video @@ -467,6 +473,8 @@ Groep beheren Kanaal beheren Geschiedenis voor nieuwe leden + Nieuwe foto instellen + Stel foto of video in Zichtbaar Nieuwe leden krijgen chatgeschiedenis ouder dan de eigen deelname te zien. Verborgen @@ -487,7 +495,10 @@ Link Tik om een permanente link toe te voegen Afbeelding kiezen + Kies foto of video + Nieuwe foto instellen Foto maken + Video opnemen Uploaden uit galerij Kies uit galerij Online zoeken @@ -619,6 +630,8 @@ un1 is lid geworden groepsafbeelding door un1 ingesteld: kanaalfoto door un1 ingesteld: + groepsvideo door un1 ingesteld: + kanaalvideo door un1 ingesteld: groepsafbeelding door un1 verwijderd kanaalfoto door un1 verwijderd bericht door un1 bewerkt: @@ -702,10 +715,13 @@ Je muziekcollectie is leeg Geen resultaten Geen resultaten voor **%1$s** in je muziekcollectie. + Geen resultaten voor **%1$s**. Muziek Onbekende artiest Onbekende titel - Shuffle + Nummer herhalen + Lijst herhalen + Lijst husselen Volgorde omdraaien Kies een bestand @@ -834,9 +850,9 @@ SPAM MELDEN Spam melden Blokkeer %1$s - BLOKKEER GEBRUIKER + BLOKKEREN SPAM MELDEN EN VERLATEN - CONTACT TOEVOEGEN + TOEVOEGEN VOEG %1$s TOE AAN CONTACTEN CONTACT BEKIJKEN **%1$s** echt blokkeren om je berichten te sturen en je te bellen via Telegram? @@ -900,16 +916,16 @@ Blijvende chatgeschiedenis Publieke links zoals t.me/titel Beheerders met aangepaste rechten - • Berichten naartoe door te sturen - • Media en bestanden te bewaren - • Te gebruiken op al je apparaten - • Snel dingen in op te zoeken + Berichten naartoe door te sturen + Media en bestanden te bewaren + Te gebruiken op al je apparaten + Snel dingen in op te zoeken Je Cloudopslag is er om: Ga naar datum Verwijder voor %1$s Verwijder voor iedereen Tekst gekopieerd naar klembord. - Hold to record audio. + Hou vast om audio op te nemen. Hou vast voor audio-opname. Tik voor video Hou vast voor video-opname. Tik voor audio Spraakbericht weggooien @@ -989,6 +1005,15 @@ Chat verbergen Weet je zeker dat je deze chat wilt verbergen? Verbergen + Stuur een bericht of tik op de groet hieronder als je klaar bent om te chatten. + %1$s is %2$s + %1$s is %2$s + Chat verplaatst naar je hoofdpagina + d + s + m + u + w %1$s heeft de zelfvernietigingstimer ingesteld op %2$s Je hebt de zelfvernietigingstimer ingesteld op %1$s @@ -1043,6 +1068,7 @@ %1$s heeft je uitgenodigd voor de groep %2$s %1$s heeft de groepsnaam van %2$s gewijzigd %1$s heeft de groepsfoto voor %2$s aangepast + %1$s heeft de groepsvideo voor %2$s aangepast %1$s heeft %3$s uitgenodigd voor de groep %2$s %1$s is terug in de groep %2$s %1$s is nu lid van de groep %2$s @@ -1147,6 +1173,11 @@ tot aan %1$s Sorry, deze groep is al vol. Sorry, deze groep bestaat niet. + Dit kanaal is privé, word lid om de inhoud ervan te bekijken. + Deze groep is privé, word lid om de inhoud ervan te bekijken. + Lid worden + Groepslid worden + LID WORDEN Link gekopieerd naar klembord. Link gekopiëerd naar klembord.\nDeze link werkt alleen voor leden van deze chat. Je kunt dit bericht niet openen omdat je geen lid bent van de chat waar dit bericht is geplaatst. @@ -1834,6 +1865,7 @@ Scan de QR-Code om met je account te verbinden. Met deze code kun je iemand toestaan om in te loggen op je Telegram-account\n\nOm de login te bevestigen ga naar, Instellingen > Apparaten > Scan QR en scan de code. Telegram heeft toegang tot je camera nodig om QR-codes te scannen. + Profielfoto instellen Lokale database Lokale database wissen @@ -2226,6 +2258,10 @@ %1$s km afstand %1$s voet afstand %1$s mijl afstand + %1$s m afstand + %1$s km afstand + %1$s ft afstand + %1$s mi afstand Aanwijzingen Geen plaatsen gevonden Geen resultaten met **%1$s** bij jou in de buurt. @@ -2328,6 +2364,7 @@ Korrel Scherper Vervagen + Huid verzachten SCHADUWEN ACCENTUEREN ALLE @@ -2343,7 +2380,11 @@ Video echt verwijderen? GIF verwijderen Echt deze GIF verwijderen? + Deze foto echt voor iedereen verwijderen? + Deze video echt voor iedereen verwijderen? + Deze GIF echt voor iedereen verwijderen? Wijzigingen weggooien? + Alle wijzigingen echt weggooien? Zoekgeschiedenis wissen? Zoekgeschiedenis echt wissen? Zoekgeschiedenis wissen @@ -2359,6 +2400,10 @@ Onderschrift Verwijder Bewerk + Pen + Markeerstift + Neon + Pijl Klonen Omlijnd Normaal @@ -2370,6 +2415,15 @@ Geef media als een enkel bericht weer Stuur zonder groeperen Stuur zonder compressie + Kies een voorpagina voor je profielvideo + Als hoofd instellen + Openen in bewerker + Dit is nu je hoofdprofielfoto. + Dit is nu je hoofdprofielvideo. + Dit is nu de hoofdkanaalfoto. + Dit is nu de hoofdkanaalvideo. + Dit is nu de hoofdgroepsfoto. + Dit is nu de hoofdgroepsvideo. Twee-staps-verificatie Twee-staps-verificatie @@ -2404,7 +2458,7 @@ Je wachtwoord voor twee-staps-verificatie is nu actief. Je tweestapsverificatiewachtwoord is aangepast. Je herstel-e-mailadres voor twee-staps-verificatie is nu actief. - Your recovery email for Two-Step Verification is changed. + Je herstel-e-mailadres voor twee-staps-verificatie is gewijzigd. Wachtwoord wijzigen Wachtwoord uitschakelen Herstel-e-mailadres instellen @@ -2435,6 +2489,7 @@ We hebben een herstelcode naar je opgegeven e-mailadres gestuurd:\n\n%1$s Controleer je E-mail en geef de 6-cijferige code in die we je hebben gestuurd. Heb je geen toegang tot je e-mailadres %1$s? + Geen toegang tot je e-mail? Bij verlies van je wachtwoord zul je je account moeten resetten. ACCOUNT RESETTEN Al je chats, berichten en alle andere data gaan verloren als je verder gaat met de account-reset. @@ -2527,8 +2582,12 @@ Niemand (+%1$d) Veiligheid Account verwijderen - Geavanceerd + Nieuwe chats van onbekende gebruikers + Archiveer en demp + Nieuwe chats, groepen en kanalen van niet-contacten automatisch archiveren. + Verwijder mijn account Verwijder mijn account indien afwezig voor + Indien afwezig voor Als je binnen deze periode niet minimaal één keer ingelogd bent geweest zal je account worden verwijderd, inclusief alle berichten en contacten. Wie kan mijn laatst gezien tijd zien? Uitzonderingen toevoegen @@ -2627,6 +2686,7 @@ un1 heeft un2 toegevoegd un1 heeft de groepsafbeelding verwijderd un1 heeft de groepsafbeelding gewijzigd + Groepsvideo door un1 gewijzigd un1 heeft de groepsnaam gewijzigd naar un2 un1 heeft de groep gemaakt Je hebt un2 verwijderd @@ -2638,6 +2698,7 @@ un1 heeft met un2 %1$s behaald Je hebt de groepsafbeelding verwijderd Je hebt de groepsafbeelding gewijzigd + Je hebt de groepsvideo gewijzigd Je hebt de groepsnaam gewijzigd naar un2 Je hebt de groep gemaakt un1 heeft je verwijderd @@ -2848,6 +2909,52 @@ %s Telegram Lokale Database %s vrijgemaakt op je apparaat! %s Andere Data + Groepsleden + Nieuwe leden per bron + Hoofdtaal van leden + Leden + Acties + Leden + Berichten + Kijkende leden + Plaatsende leden + %1$d symbolen + %1$d symbool + %1$d symbolen + %1$d symbolen + %1$d symbolen + %1$d symbolen + %1$d verwijderingen + %1$d verwijdering + %1$d verwijderingen + %1$d verwijderingen + %1$d verwijderingen + %1$d verwijderingen + %1$d verbanningen + %1$d verbanning + %1$d verbanningen + %1$d verbanningen + %1$d verbanningen + %1$d verbanningen + %1$d beperkingen + %1$d beperking + %1$d beperkingen + %1$d beperkingen + %1$d beperkingen + %1$d beperkingen + %1$d uitnodigingen + %1$d uitnodiging + %1$d uitnodigingen + %1$d uitnodigingen + %1$d uitnodigingen + %1$d uitnodigingen + Actiefste beheerders + Actiefste leden + Actiefste uitnodigers + %s per bericht + Actiefste dagen + Berichten weergeven + Profiel openen Telegram Snel @@ -3392,6 +3499,7 @@ Geheime chat Gestuurd %s Ontvangen %s + Gepland om %s Ga terug Navigatiemenu openen %2$s door %1$s @@ -3448,6 +3556,7 @@ Paneel inklappen Gebruikersopties Draaien + Spiegel Fotobewerker Aanpassingen Fotoviewer @@ -3462,6 +3571,8 @@ Nog een foto nemen Gezien Niet gezien + Versturen + Fout bij versturen Niet afgespeeld Volgende zoekresultaat Vorig zoekresultaat @@ -3472,6 +3583,36 @@ Laat los voor archief Kleiner formaat Betere kwaliteit + %1$s van %2$s + %1$s van %2$s gedownload + %1$s van %2$s geüpload + %1$d keer bekeken + %1$d keer bekeken + %1$d keer bekeken + %1$d keer bekeken + %1$d keer bekeken + Send %1$d photo + Send %1$d photos + Send %1$d photos + Send %1$d photos + Send %1$d photos + Send %1$d file + Send %1$d files + Send %1$d files + Send %1$d files + Send %1$d files + Send %1$d audio file + Send %1$d audio files + Send %1$d audio files + Send %1$d audio files + Send %1$d audio files + Share in %1$d chat + Share in %1$d chats + Share in %1$d chats + Share in %1$d chats + Share in %1$d chats + Juist antwoord + Picture-in-Picture mode dd MMM yyyy, h:mm a dd MMM yyyy, HH:mm diff --git a/TMessagesProj/src/main/res/values-pt-rBR/strings.xml b/TMessagesProj/src/main/res/values-pt-rBR/strings.xml index 6732aaac2..34ecdd9ba 100644 --- a/TMessagesProj/src/main/res/values-pt-rBR/strings.xml +++ b/TMessagesProj/src/main/res/values-pt-rBR/strings.xml @@ -37,7 +37,7 @@ Não recebeu o código? Enviar o código por SMS Cancelar Exclusão da Conta - Alguém com acesso ao seu número de telefone **%1$s** solicitou a exclusão de sua conta do Telegram e a redefinição de sua senha de Verificação em Duas Etapas.\n\nSe não foi você, por favor insira o código que te enviamos via SMS. + Alguém com acesso ao número **%1$s** solicitou excluir a sua conta do Telegram e redefinir a sua senha de Verificação em Duas Etapas.\n\nSe não foi você, digite o código que acabamos de enviar via SMS. Você também pode cancelar isso *alterando o seu número de telefone*. Apagar conta Como a conta %1$s está ativa e protegida por uma senha, iremos apagá-la em 1 semana por questões de segurança.\n\nVocê pode cancelar esse processo a qualquer momento. Você poderá redefinir a sua conta em: @@ -125,7 +125,7 @@ Visualizado recentemente ESCONDER Comece a conversar ao tocar no botão de lápis no canto inferior direito. - Comece a conversar pressionando o botão de lápis no canto inferior direito. + Comece a conversar ao tocar no botão de lápis no canto inferior direito. Aguardando rede... Conectando... Conectado @@ -223,6 +223,9 @@ Chats fixados Você pode fixar uma quantidade ilimitada de chats arquivados no topo. Toque e segure neste botão para agendar a mensagem ou enviar sem som. + Arquivar novos chats? + Parece que você está recebendo muitas mensagens de pessoas que não estão na sua agenda. Quer **silenciar** e **arquivar** essas conversas automaticamente? + ABRIR CONFIGURAÇÕES Promover a administrador Editar permissões de admin @@ -326,6 +329,7 @@ Bots Outros membros Outros inscritos + entrou em %1$s Usuários restringidos Administradores Apagar Canal @@ -339,6 +343,7 @@ Por favor, escolha um link para o seu canal público, assim as pessoas poderão encontrá-lo na busca e compartilhar com outros.\n\nSe não estiver interessado, sugerimos que crie um canal privado. Canal criado Foto do canal alterada + Vídeo do canal alterado Foto do canal removida Nome do canal alterado para un2 Desculpe, você reservou muitos nomes públicos. Você pode remover o link público de um de seus grupos ou canais, ou criar de forma privada. @@ -377,6 +382,7 @@ Desculpe, você não pode enviar mensagens para esse canal. %1$s adicionou você ao canal %2$s Canal %1$s atualizou a foto + O canal %1$s atualizou o vídeo dele %1$s postou uma mensagem %1$s postou uma foto %1$s postou um vídeo @@ -467,6 +473,8 @@ Gerenciar grupo Gerenciar canal Histórico do chat para novos membros + Definir Nova Foto + Definir Foto ou Vídeo Visível Novos membros verão mensagens que foram enviadas antes de eles entrarem. Oculto @@ -487,7 +495,10 @@ Link Toque para adicionar um link permanente Escolher Foto + Escolha uma foto ou vídeo + Definir nova foto Tirar foto + Gravar vídeo Enviar da Galeria Selecionar da Galeria Buscar na Internet @@ -619,6 +630,8 @@ un1 entrou no canal un1 definiu uma nova foto do grupo un1 definiu uma nova foto do canal + un1 definiu um novo vídeo para o grupo + un1 definiu um novo vídeo para o canal un1 removeu a foto do grupo un1 removeu foto do canal un1 editou esta mensagem: @@ -651,7 +664,7 @@ un1 editou a descrição do canal Descrição anterior un1 tornou o histórico do grupo visível para novos membros - un1 tornou o histórico do grupo escondido para novos membros + un1 ocultou o histórico do grupo para novos membros un1 ativou o convite ao grupo un1 desativou o convite ao grupo un1 ativou as assinaturas @@ -702,10 +715,13 @@ A sua biblioteca de músicas está vazia. Nenhum resultado encontrado Não há nenhuma música que corresponda a **%1$s** em sua biblioteca. + Nenhum resultado para **%1$s**. Música Artista desconhecido Título desconhecido - Aleatório + Repetir música + Repetir lista + Misturar lista Ordem reversa Selecionar Arquivo @@ -989,6 +1005,15 @@ Ocultar chat Deseja realmente ocultar esse chat? Ocultar + Envie uma mensagem ou toque no sticker para dizer olá e mostrar que você quer conversar. + %1$s está a %2$s + %1$s está a %2$s + O chat foi movido para a lista principal. + d + s + m + h + S "%1$s alterou o timer de autodestruição para %2$s " Você alterou o timer de autodestruição para %1$s @@ -1043,6 +1068,7 @@ %1$s te convidou para o grupo %2$s %1$s renomeou o grupo %2$s %1$s alterou a foto do grupo para %2$s + %1$s alterou o vídeo do grupo para %2$s %1$s convidou %3$s para o grupo %2$s %1$s retornou ao grupo %2$s %1$s entrou no grupo %2$s @@ -1147,6 +1173,11 @@ até %1$s Desculpe, esse grupo está lotado. Desculpe, parece que esse chat não existe. + Esse canal é privado. Por favor, inscreva-se para continuar visualizando o conteúdo. + Esse grupo é privado. Por favor, entre para continuar visualizando o conteúdo. + Entrar no Canal + Entrar no Grupo + ENTRAR Link copiado. Link copiado.\nEle só funciona para membros deste chat. Desculpe, você não faz parte da conversa em que essa mensagem foi postada e infelizmente não pode acessá-la. @@ -1233,7 +1264,7 @@ Desconhecido Número oculto O número de telefone ficará visível assim que %1$s te adicionar como contato. - Quando você tocar em **CONCLUÍDO**, o seu número ficará visível para %1$s. + Quando você tocar em **PRONTO**, o seu número ficará visível para %1$s. Compartilhar meu número com %1$s %1$s agora está em seus Contatos. Info @@ -1452,7 +1483,7 @@ Todas as configurações de notificação foram redefinidas Redefinir todas as notificações - Tem certeza de que deseja restaurar todas as configurações de notificação para o padrão? + Deseja redefinir todas as configurações de notificação para o padrão? Tamanho do texto das mensagens Fazer uma Pergunta Adicionar uma explicação @@ -1476,8 +1507,8 @@ Vibração no Aplicativo Vibração Prévias no Aplicativo - Restaurar - Restaurar Configurações + "Redefinir " + "Redefinir Notificações " Desfaz todas as configurações de notificação para todos os seus chats. Notificações e Sons Notificações Personalizadas @@ -1658,7 +1689,7 @@ Sem mídia Sem GIFs Apagar Configurações de Download - Restaurar configurações + Redefinir configurações Tem certeza de que deseja apagar as configurações de download automático? Ao usar dados móveis Quando conectado ao Wi-Fi @@ -1723,7 +1754,7 @@ Emoji Grande Usar emoji padrão do sistema Telegram para Android %1$s - Debug Menu + Menu de Depuração Enviar Logs Limpar Logs Ativar Logs @@ -1834,6 +1865,7 @@ Escaneie o código QR para conectar à sua conta. Esse código pode ser usado para permitir que alguém faça login em sua conta do Telegram.\n\nPara confirmar o login no Telegram, acesse Configurações > Dispositivos > Ler Código QR e escaneie o código. O Telegram precisa de acesso à sua câmera para que você possa escanear códigos QR. + Definir Foto do Perfil Base de Dados Local Limpar base de dados local @@ -1995,7 +2027,7 @@ Seu nome Seu nome em %1$s Seu nome no idioma do seu país de residência (%1$s). - Por favor, verifique se este nome está correto:\n\n%1$s %2$s %3$s + Por favor, confirme se este nome está correto:\n\n%1$s %2$s %3$s Árabe Azerbaijanês Búlgaro @@ -2226,6 +2258,10 @@ %1$s km de distância %1$s pés de distância %1$s milhas de distância + %1$s m de você + %1$s km de você + %1$s pés de você + %1$s milhas de você Direções Nenhum lugar encontrado. Não há nada relacionado com **%1$s** perto de você. @@ -2246,7 +2282,7 @@ Ou escolha um lugar Toque para enviar este local Localizações em Tempo Real - Loc. em Tempo Real + Localiz. em Tempo Real por 15 minutos por 1 hora por 8 horas @@ -2254,9 +2290,9 @@ atualizado %1$s atualizado agora Você e %1$s - %1$s em %2$s + %1$s com %2$s PARAR TODOS - Você está compartilhando sua Localização em %1$s + Compartilhando a Localização com %1$s Escolha por quanto tempo %1$s verá sua localização precisa. Escolha por quanto tempo as pessoas nesse chat irão ver a sua localização em tempo real. Ativar GPS @@ -2291,7 +2327,7 @@ Mostrar todos os arquivos Mostrar todos os GIFs Toque para baixar - Abrir arquivo + Abrir Arquivo... Mostrar na conversa Parar download Salvar na galeria @@ -2323,11 +2359,12 @@ Exposição Calor Saturação - Vignette + Vinheta Sombras Granulado Nitidez Fade + Suavizar Pele SOMBRAS LUZES TUDO @@ -2343,7 +2380,11 @@ Tem certeza de que deseja apagar este vídeo? Apagar GIF Tem certeza de que deseja apagar esse GIF? + Deseja mesmo apagar essa foto para todos? + Deseja mesmo apagar esse vídeo para todos? + Deseja mesmo apagar esse GIF para todos? Descartar mudanças? + Deseja mesmo descartar todas as alterações? Limpar histórico de busca? Deseja limpar o seu histórico de busca? Limpar histórico de busca @@ -2359,6 +2400,10 @@ Legenda Apagar Editar + Caneta + Marcador + Neon + Seta Duplicar Delineado Regular @@ -2370,6 +2415,15 @@ Mostrar mídias em uma só mensagem Enviar sem agrupar Enviar sem compressão + Escolha a capa para o seu vídeo de perfil + Usar no perfil + Abrir no editor + Esta é a foto principal do seu perfil agora. + Este é o vídeo principal do seu perfil agora. + Esta é a foto principal do canal agora. + Este é o vídeo principal do canal agora. + Esta é a foto principal do grupo agora. + Este é o vídeo principal do grupo agora. Verificação em Duas Etapas Verificação em Duas Etapas @@ -2435,6 +2489,7 @@ O código de recuperação foi enviado para o email fornecido:\n\n%1$s Por favor, verifique o seu email e digite o código de 6 dígitos que te enviamos. Está tendo problemas para acessar seu email %1$s? + Problemas ao acessar o seu email? Se você não puder acessar o seu email, as suas únicas opções são lembrar a senha ou apagar a sua conta. APAGAR MINHA CONTA Se você prosseguir e apagar a sua conta, você perderá todos os seus chats e mensagens, assim como todas as suas mídias e arquivos compartilhados. @@ -2471,10 +2526,10 @@ Chamadas Recebidas Tempo total Total - Reiniciar Estatísticas + Redefinir Estatísticas Uso da rede desde %1$s - Reiniciar estatísticas - Deseja reiniciar suas estatísticas de uso? + Redefinir estatísticas + Deseja redefinir as estatísticas de uso? Privacidade e Segurança Privacidade @@ -2527,8 +2582,12 @@ Ninguém (+%1$d) Segurança Autodestruição da conta - Avançado + Novos chats de usuários desconhecidos + Arquivar e Silenciar + Automaticamente arquiva e silencia novas conversas, grupos e canais de não contatos. + Apagar minha conta Apagar minha conta se inativa por + Se inativa por Se você não acessar sua conta ao menos uma vez nesse período, sua conta será apagada junto com todas as suas mensagens e contatos. Quem pode ver o seu Último Acesso? Definir exceções @@ -2581,7 +2640,7 @@ Parar bot Reiniciar bot - Concluído + Pronto Abrir Salvar Cancelar @@ -2627,6 +2686,7 @@ un1 adicionou un2 un1 removeu a foto do grupo un1 alterou a foto do grupo + un1 alterou o vídeo do grupo un1 alterou o nome do grupo para un2 un1 criou o grupo Você removeu un2 @@ -2638,6 +2698,7 @@ un1 marcou %1$s em un2 Você removeu a foto do grupo Você alterou a foto do grupo + Você alterou o vídeo do grupo Você alterou o nome do grupo para un2 Você criou o grupo un1 removeu você @@ -2848,6 +2909,52 @@ %s Base de Dados Local %s liberados no seu dispositivo! Outros Dados • %s + Membros do grupo + Novos membros por origem + Idioma principal dos membros + Mensagens + Ações + Membros + Mensagens + Membros que visualizam + Membros que postam + %1$d símbolos + %1$d símbolo + %1$d símbolos + %1$d símbolos + %1$d símbolos + %1$d símbolos + %1$d exclusões + %1$d exclusão + %1$d exclusões + %1$d exclusões + %1$d exclusões + %1$d exclusões + %1$d bans + %1$d ban + %1$d bans + %1$d bans + %1$d bans + %1$d bans + %1$d restrições + %1$d restrição + %1$d restrições + %1$d restrições + %1$d restrições + %1$d restrições + %1$d convites + %1$d convite + %1$d convites + %1$d convites + %1$d convites + %1$d convites + Top admins + Top membros + Top promotores + %s por mensagem + Top dias da semana + Ver Mensagens + Ver Perfil Telegram Rápido @@ -2877,12 +2984,12 @@ Linha ocupada Chamada do Telegram Chamada do Telegram em andamento - Encerrar chamada + Encerrar Chamada Outra chamada em andamento Você está em uma chamada com **%1$s**. Gostaria de desligar e iniciar uma nova com **%2$s**? Chamadas de voz Toque - Você pode personalizar o toque usado quando esse contato te chamar no Telegram + Você pode personalizar o toque usado quando esse contato te ligar no Telegram. Chamadas Chamadas de ponta a ponta serão (ou não) estabelecidas com esses usuários, ignorando as opções acima. Usar ponta a ponta com @@ -3025,18 +3132,18 @@ %1$d membros %1$d membros %1$d membros - e mais %1$d estão escrevendo - e mais %1$d estão escrevendo - e mais %1$d estão escrevendo - e mais %1$d estão escrevendo - e mais %1$d estão escrevendo - e mais %1$d estão escrevendo - %1$s e mais %2$d estão escrevendo - %1$s e mais %2$d estão escrevendo - %1$s e mais %2$d estão escrevendo - %1$s e mais %2$d estão escrevendo - %1$s e mais %2$d estão escrevendo - %1$s e mais %2$d estão escrevendo + e mais %1$d escrevendo + e mais %1$d escrevendo + e mais %1$d escrevendo + e mais %1$d escrevendo + e mais %1$d escrevendo + e mais %1$d escrevendo + %1$s e mais %2$d escrevendo + %1$s e mais %2$d escrevendo + %1$s e mais %2$d escrevendo + %1$s e mais %2$d escrevendo + %1$s e mais %2$d escrevendo + %1$s e mais %2$d escrevendo %1$d novas mensagens %1$d nova mensagem %1$d novas mensagens @@ -3392,6 +3499,7 @@ Chat secreto Enviado %s Recebido %s + Agendado para %s Voltar Abrir menu de navegação %2$s por %1$s @@ -3448,6 +3556,7 @@ Colapsar painel Opções do usuário Girar + Espelhar Editor de fotos Ajustes Visualizador de fotos @@ -3462,6 +3571,8 @@ Tirar mais uma foto Lido Não lido + Enviando + Erro no envio Não reproduzido Próximo resultado de busca Resultado anterior de busca @@ -3472,6 +3583,36 @@ Solte para ver o arquivo Tamanho menor Qualidade maior + %1$s de %2$s + Baixados %1$s de %2$s + Enviados %1$s de %2$s + Visualizado %1$d vez + Visualizado %1$d vezes + Visualizado %1$d vezes + Visualizado %1$d vezes + Visualizado %1$d vezes + Enviar %1$d foto + Enviar %1$d fotos + Enviar %1$d fotos + Enviar %1$d fotos + Enviar %1$d fotos + Enviar %1$d arquivo + Enviar %1$d arquivos + Enviar %1$d arquivos + Enviar %1$d arquivos + Enviar %1$d arquivos + Enviar %1$d áudio + Enviar %1$d áudios + Enviar %1$d áudios + Enviar %1$d áudios + Enviar %1$d áudios + Encaminhar em %1$d chat + Encaminhar em %1$d chats + Encaminhar em %1$d chats + Encaminhar em %1$d chats + Encaminhar em %1$d chats + Resposta correta + Modo de Janela Flutuante dd \'de\' MMM \'de\' yyyy, h:mm a dd \'de\' MMM \'de\' yyyy, HH:mm diff --git a/TMessagesProj/src/main/res/values/strings.xml b/TMessagesProj/src/main/res/values/strings.xml index 1f0f2cc19..c36c9fa88 100644 --- a/TMessagesProj/src/main/res/values/strings.xml +++ b/TMessagesProj/src/main/res/values/strings.xml @@ -37,7 +37,7 @@ Didn\'t get the code? Send the code as an SMS Cancel account reset - Somebody with access to your phone number **%1$s** has requested to delete your Telegram account and reset your 2-Step Verification password.\n\nIf this wasn\'t you, please enter the code we\'ve just sent you via SMS to your number. + Somebody with access to **%1$s** has requested to delete your Telegram account and reset your 2-Step Verification password.\n\nIf this wasn\'t you, please enter the code we\'ve just sent you via SMS. You can also cancel this by *changing your phone number*. Reset account Since the account **%1$s** is active and protected by a password, we will delete it in 1 week for security purposes.\n\nYou can cancel this process at any time. You\'ll be able to reset your account in: @@ -223,6 +223,9 @@ Pinned chats You can pin an unlimited number of archived chats to the top. Hold this button to schedule your message or send it without sound. + Hide new chats? + You are receiving many new chats from users who are not in your Contacts List. Do you want have such chats **automatically muted** and **archived**? + GO TO SETTINGS Promote to admin Edit admin rights @@ -326,6 +329,7 @@ Bots Other members Other subscribers + joined %1$s Restricted users Administrators Delete Channel @@ -339,6 +343,7 @@ Please choose a link for your public channel, so that people can find it in search and share with others.\n\nIf you\'re not interested, we suggest creating a private channel instead. Channel created Channel photo changed + Channel video changed Channel photo removed Channel name changed to un2 Sorry, you have reserved too many public usernames. You can revoke the link from one of your older groups or channels, or create a private entity instead. @@ -377,6 +382,7 @@ Sorry, you can\'t send messages to this channel. %1$s added you to the channel %2$s Channel %1$s updated its photo + Channel %1$s updated its video %1$s posted a message %1$s posted a photo %1$s posted a video @@ -467,6 +473,8 @@ Manage group Manage channel Chat history for new members + Set New Photo + Set Photo or Video Visible New members will see messages that were sent before they joined. Hidden @@ -487,7 +495,10 @@ Link Tap to add a permanent link Choose photo + Choose photo or video + Set new photo Take photo + Record video Upload from gallery Select from gallery Search web @@ -619,6 +630,8 @@ un1 joined the channel un1 set a new group photo un1 set a new channel photo + un1 set a new group video + un1 set a new channel video un1 removed the group photo un1 removed the channel photo un1 edited this message: @@ -702,10 +715,13 @@ Your music library is empty. No results found There are no matches with **%1$s** in your music library. + There are no matches for **%1$s**. Music Unknown artist Unknown title - Shuffle + Repeat song + Repeat list + Shuffle list Reverse order Select File @@ -989,6 +1005,15 @@ Hide chat Are you sure you want to hide the selected chat? Hide + Send a message or tap on the greeting below to show that you are ready to chat + %1$s is in %2$s + %1$s is %2$s + The chat was moved to your main list. + d + s + m + h + w %1$s set the self-destruct timer to %2$s You set the self-destruct timer to %1$s @@ -1043,6 +1068,7 @@ %1$s invited you to the group %2$s %1$s renamed the group %2$s %1$s changed the group photo for %2$s + %1$s changed the group video for %2$s %1$s invited %3$s to the group %2$s %1$s returned to the group %2$s %1$s joined the group %2$s @@ -1147,6 +1173,11 @@ up to %1$s Sorry, this group is already full. Sorry, this chat does not seem to exist. + This channel is private. Please join it to continue viewing its content. + This group is private. Please join it to continue viewing its content. + Join Channel + Join Group + JOIN Link copied to clipboard Link copied to clipboard.\nThis link will only work for members of this chat. Unfortunately, you can\'t access this message. You are not a member of the chat where it was posted. @@ -1834,6 +1865,7 @@ Scan the QR code to connect your account. This code can be used to allow someone to log in to your Telegram account.\n\nTo confirm Telegram login, please go to Settings > Devices > Scan QR and scan the code. Telegram needs access to your camera so that you can scan QR codes. + Set Profile Photo Local Database Clear local database @@ -2226,6 +2258,10 @@ %1$s km away %1$s ft away %1$s mi away + %1$s m from you + %1$s km from you + %1$s ft from you + %1$s mi from you Directions No places found There are no matches with **%1$s** near you. @@ -2328,6 +2364,7 @@ Grain Sharpen Fade + Soften Skin SHADOWS HIGHLIGHTS ALL @@ -2343,7 +2380,11 @@ Are you sure you want to delete this video? Delete GIF Are you sure you want to delete this GIF? + Are you sure you want to delete this photo for everyone? + Are you sure you want to delete this video for everyone? + Are you sure you want to delete this GIF for everyone? Discard changes? + Are you sure you want to discard all changes? Clear search history? Are you sure you want to clear your search history? Clear search history @@ -2359,6 +2400,10 @@ Caption Delete Edit + Pen + Marker + Neon + Arrow Duplicate Outlined Regular @@ -2370,6 +2415,15 @@ Group media into one message Send without grouping Send without compression + Choose a cover for your profile video + Set as main + Open in editor + This is your main profile photo now. + This is your main profile video now. + This is the main channel photo now. + This is the main channel video now. + This is the main group photo now. + This is the main group video now. Two-Step Verification Two-Step Verification @@ -2435,6 +2489,7 @@ We have sent a recovery code to the email address you provided:\n\n%1$s Please check your email and enter the 6-digit code we sent you. Having trouble accessing your email %1$s? + Having trouble accessing your email? If you can\'t restore access to your email, your remaining options are either to remember your password or to reset your account. RESET MY ACCOUNT You will lose all your chats and messages, along with any media and files you shared, if you proceed with resetting your account. @@ -2527,8 +2582,12 @@ Nobody (+%1$d) Security Account self-destructs - Advanced + New chats from unknown users + Archive and Mute + Automatically archive and mute new chats, groups and channels from non-contacts. + Delete my account Delete my account if away for + If away for If you do not come online at least once within this period, your account will be deleted along with all messages and contacts. Who can see your Last Seen time? Add exceptions @@ -2627,6 +2686,7 @@ un1 added un2 un1 removed the group photo un1 changed the group photo + un1 changed the group video un1 changed the group name to un2 un1 created the group You removed un2 @@ -2638,6 +2698,7 @@ un1 scored %1$s in un2 You removed the group photo You changed the group photo + You changed the group video You changed the group name to un2 You created the group un1 removed you @@ -2848,6 +2909,52 @@ %s Telegram Local Database %s freed on your device! %s Other Data + Group members + New members by source + Members\'s primary language + Messages + Actions + Members + Messages + Viewing Members + Posting Members + %1$d symbols + %1$d symbol + %1$d symbols + %1$d symbols + %1$d symbols + %1$d symbols + %1$d deletions + %1$d deletion + %1$d deletions + %1$d deletions + %1$d deletions + %1$d deletions + %1$d bans + %1$d ban + %1$d bans + %1$d bans + %1$d bans + %1$d bans + %1$d restrictions + %1$d restriction + %1$d restrictions + %1$d restrictions + %1$d restrictions + %1$d restrictions + %1$d invitations + %1$d invitation + %1$d invitations + %1$d invitations + %1$d invitations + %1$d invitations + Top admins + Top members + Top inviters + %s per message + Top days of week + View Messages + Open Profile Telegram Fast @@ -3392,6 +3499,7 @@ Secret chat Sent %s Received %s + Scheduled for %s Go back Open navigation menu %2$s by %1$s @@ -3448,6 +3556,7 @@ Collapse panel User options Rotate + Mirror Photo editor Adjustments Photo viewer @@ -3462,6 +3571,8 @@ Take one more picture Seen Not seen + Sending + Sending error Not played Next search result Previous search result @@ -3472,6 +3583,36 @@ Release for archive Smaller size Higher quality + %1$s of %2$s + Downloaded %1$s of %2$s + Uploaded %1$s of %2$s + Viewed %1$d time + Viewed %1$d times + Viewed %1$d times + Viewed %1$d times + Viewed %1$d times + Send %1$d photo + Send %1$d photos + Send %1$d photos + Send %1$d photos + Send %1$d photos + Send %1$d file + Send %1$d files + Send %1$d files + Send %1$d files + Send %1$d files + Send %1$d audio file + Send %1$d audio files + Send %1$d audio files + Send %1$d audio files + Send %1$d audio files + Share in %1$d chat + Share in %1$d chats + Share in %1$d chats + Share in %1$d chats + Share in %1$d chats + Correct answer + Picture-in-Picture mode MMM dd yyyy, h:mm a MMM dd yyyy, HH:mm diff --git a/TMessagesProj/src/main/res/xml/shortcuts.xml b/TMessagesProj/src/main/res/xml/shortcuts.xml new file mode 100644 index 000000000..a7ff4be90 --- /dev/null +++ b/TMessagesProj/src/main/res/xml/shortcuts.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file